openvsx-webui-test 0.20.1-rc.0 → 0.20.2-dev.1

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 (94) hide show
  1. package/lib/components/scan-admin/scan-card/scan-card-expanded-content.d.ts.map +1 -1
  2. package/lib/components/scan-admin/scan-card/scan-card-expanded-content.js +28 -4
  3. package/lib/components/scan-admin/scan-card/scan-card-expanded-content.js.map +1 -1
  4. package/lib/components/sidepanel/navigation-item.d.ts +0 -1
  5. package/lib/components/sidepanel/navigation-item.d.ts.map +1 -1
  6. package/lib/components/sidepanel/navigation-item.js +22 -8
  7. package/lib/components/sidepanel/navigation-item.js.map +1 -1
  8. package/lib/components/sidepanel/sidebar-context.d.ts +16 -0
  9. package/lib/components/sidepanel/sidebar-context.d.ts.map +1 -0
  10. package/lib/components/sidepanel/sidebar-context.js +15 -0
  11. package/lib/components/sidepanel/sidebar-context.js.map +1 -0
  12. package/lib/components/sidepanel/sidepanel.d.ts +4 -4
  13. package/lib/components/sidepanel/sidepanel.d.ts.map +1 -1
  14. package/lib/components/sidepanel/sidepanel.js +47 -10
  15. package/lib/components/sidepanel/sidepanel.js.map +1 -1
  16. package/lib/context/scan-admin/scan-api-effects.d.ts.map +1 -1
  17. package/lib/context/scan-admin/scan-api-effects.js +1 -0
  18. package/lib/context/scan-admin/scan-api-effects.js.map +1 -1
  19. package/lib/context/scan-admin/scan-types.d.ts +2 -0
  20. package/lib/context/scan-admin/scan-types.d.ts.map +1 -1
  21. package/lib/context/scan-admin/scan-types.js.map +1 -1
  22. package/lib/extension-registry-service.d.ts +6 -0
  23. package/lib/extension-registry-service.d.ts.map +1 -1
  24. package/lib/extension-registry-service.js +17 -0
  25. package/lib/extension-registry-service.js.map +1 -1
  26. package/lib/hooks/use-local-storage.d.ts +23 -0
  27. package/lib/hooks/use-local-storage.d.ts.map +1 -0
  28. package/lib/hooks/use-local-storage.js +62 -0
  29. package/lib/hooks/use-local-storage.js.map +1 -0
  30. package/lib/pages/admin-dashboard/admin-dashboard.d.ts.map +1 -1
  31. package/lib/pages/admin-dashboard/admin-dashboard.js +39 -95
  32. package/lib/pages/admin-dashboard/admin-dashboard.js.map +1 -1
  33. package/lib/pages/admin-dashboard/admin-header.d.ts +19 -0
  34. package/lib/pages/admin-dashboard/admin-header.d.ts.map +1 -0
  35. package/lib/pages/admin-dashboard/admin-header.js +16 -0
  36. package/lib/pages/admin-dashboard/admin-header.js.map +1 -0
  37. package/lib/pages/admin-dashboard/admin-sidepanel.d.ts +19 -0
  38. package/lib/pages/admin-dashboard/admin-sidepanel.d.ts.map +1 -0
  39. package/lib/pages/admin-dashboard/admin-sidepanel.js +17 -0
  40. package/lib/pages/admin-dashboard/admin-sidepanel.js.map +1 -0
  41. package/lib/pages/admin-dashboard/namespace-admin.d.ts.map +1 -1
  42. package/lib/pages/admin-dashboard/namespace-admin.js +4 -1
  43. package/lib/pages/admin-dashboard/namespace-admin.js.map +1 -1
  44. package/lib/pages/admin-dashboard/namespace-change-dialog.d.ts.map +1 -1
  45. package/lib/pages/admin-dashboard/namespace-change-dialog.js +6 -1
  46. package/lib/pages/admin-dashboard/namespace-change-dialog.js.map +1 -1
  47. package/lib/pages/admin-dashboard/namespace-delete-dialog.d.ts +23 -0
  48. package/lib/pages/admin-dashboard/namespace-delete-dialog.d.ts.map +1 -0
  49. package/lib/pages/admin-dashboard/namespace-delete-dialog.js +53 -0
  50. package/lib/pages/admin-dashboard/namespace-delete-dialog.js.map +1 -0
  51. package/lib/pages/admin-dashboard/nav-types.d.ts +27 -0
  52. package/lib/pages/admin-dashboard/nav-types.d.ts.map +1 -0
  53. package/lib/pages/admin-dashboard/nav-types.js +14 -0
  54. package/lib/pages/admin-dashboard/nav-types.js.map +1 -0
  55. package/lib/pages/admin-dashboard/scan-admin.d.ts.map +1 -1
  56. package/lib/pages/admin-dashboard/scan-admin.js +1 -4
  57. package/lib/pages/admin-dashboard/scan-admin.js.map +1 -1
  58. package/lib/pages/admin-dashboard/welcome.d.ts +5 -1
  59. package/lib/pages/admin-dashboard/welcome.d.ts.map +1 -1
  60. package/lib/pages/admin-dashboard/welcome.js +18 -16
  61. package/lib/pages/admin-dashboard/welcome.js.map +1 -1
  62. package/lib/pages/user/user-namespace-details.d.ts.map +1 -1
  63. package/lib/pages/user/user-namespace-details.js +2 -3
  64. package/lib/pages/user/user-namespace-details.js.map +1 -1
  65. package/lib/pages/user/user-setting-tabs.d.ts.map +1 -1
  66. package/lib/pages/user/user-setting-tabs.js +1 -1
  67. package/lib/pages/user/user-setting-tabs.js.map +1 -1
  68. package/lib/pages/user/user-settings-namespace-detail.d.ts +1 -0
  69. package/lib/pages/user/user-settings-namespace-detail.d.ts.map +1 -1
  70. package/lib/pages/user/user-settings-namespace-detail.js +18 -2
  71. package/lib/pages/user/user-settings-namespace-detail.js.map +1 -1
  72. package/lib/pages/user/user-settings-namespaces.js.map +1 -1
  73. package/package.json +2 -4
  74. package/src/components/scan-admin/scan-card/scan-card-expanded-content.tsx +50 -5
  75. package/src/components/sidepanel/navigation-item.tsx +79 -23
  76. package/src/components/sidepanel/sidebar-context.tsx +17 -0
  77. package/src/components/sidepanel/sidepanel.tsx +57 -29
  78. package/src/context/scan-admin/scan-api-effects.ts +1 -0
  79. package/src/context/scan-admin/scan-types.ts +2 -0
  80. package/src/extension-registry-service.ts +18 -0
  81. package/src/hooks/use-local-storage.ts +67 -0
  82. package/src/pages/admin-dashboard/admin-dashboard.tsx +48 -197
  83. package/src/pages/admin-dashboard/admin-header.tsx +59 -0
  84. package/src/pages/admin-dashboard/admin-sidepanel.tsx +59 -0
  85. package/src/pages/admin-dashboard/namespace-admin.tsx +5 -0
  86. package/src/pages/admin-dashboard/namespace-change-dialog.tsx +55 -46
  87. package/src/pages/admin-dashboard/namespace-delete-dialog.tsx +89 -0
  88. package/src/pages/admin-dashboard/nav-types.ts +31 -0
  89. package/src/pages/admin-dashboard/scan-admin.tsx +1 -4
  90. package/src/pages/admin-dashboard/welcome.tsx +80 -48
  91. package/src/pages/user/user-namespace-details.tsx +4 -5
  92. package/src/pages/user/user-setting-tabs.tsx +3 -2
  93. package/src/pages/user/user-settings-namespace-detail.tsx +37 -4
  94. package/src/pages/user/user-settings-namespaces.tsx +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"user-settings-namespaces.js","sourceRoot":"","sources":["../../../src/pages/user/user-settings-namespaces.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"user-settings-namespaces.js","sourceRoot":"","sources":["../../../src/pages/user/user-settings-namespaces.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvsx-webui-test",
3
- "version": "0.20.1-rc.0",
3
+ "version": "0.20.2-dev.1",
4
4
  "description": "User interface for Eclipse Open VSX",
5
5
  "keywords": [
6
6
  "react",
@@ -39,7 +39,6 @@
39
39
  "node": ">=22.0.0"
40
40
  },
41
41
  "dependencies": {
42
- "@babel/core": "^7.29.0",
43
42
  "@emotion/react": "^11.11.1",
44
43
  "@emotion/styled": "^11.11.0",
45
44
  "@mdit/plugin-alert": "^0.22.3",
@@ -61,7 +60,7 @@
61
60
  "prop-types": "^15.8.1",
62
61
  "punycode": "^2.3.0",
63
62
  "react": "^18.2.0",
64
- "react-avatar-editor": "^13.0.0",
63
+ "react-avatar-editor": "^15.0.0",
65
64
  "react-dom": "^18.2.0",
66
65
  "react-dropzone": "^14.2.3",
67
66
  "react-helmet-async": "^2.0.5",
@@ -91,7 +90,6 @@
91
90
  "@types/prop-types": "^15.7.0",
92
91
  "@types/punycode": "^2.1.0",
93
92
  "@types/react": "^18.2.0",
94
- "@types/react-avatar-editor": "^13.0.0",
95
93
  "@types/react-dom": "^18.2.0",
96
94
  "@types/react-infinite-scroller": "^1.2.0",
97
95
  "@types/react-router-dom": "^5.3.0",
@@ -12,7 +12,8 @@
12
12
  ********************************************************************************/
13
13
 
14
14
  import { FC } from 'react';
15
- import { Box, Typography, Collapse, Chip } from '@mui/material';
15
+ import { Box, Typography, Collapse, Chip, Link } from '@mui/material';
16
+ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
16
17
  import { useTheme, Theme } from '@mui/material/styles';
17
18
  import { ScanResult, Threat, ValidationFailure, CheckResult } from '../../../context/scan-admin';
18
19
  import { ScanDetailCard } from './scan-detail-card';
@@ -127,17 +128,22 @@ const CheckResultItem: FC<CheckResultItemProps> = ({ checkResult }) => {
127
128
  // Non-required errors get striped styling to indicate they didn't block publishing
128
129
  const isOptionalError = isError && checkResult.required === false;
129
130
 
131
+ // Scanner rows often carry long verdict text while publish checks just show
132
+ // short statuses like "No issues found"
133
+ const isScannerJob = checkResult.category === 'SCANNER_JOB';
134
+ const showSummaryInMiddle = isScannerJob && !!checkResult.summary;
135
+
130
136
  return (
131
137
  <Box sx={{
132
138
  display: 'flex',
133
139
  alignItems: 'center',
134
- justifyContent: 'space-between',
140
+ gap: 2,
135
141
  p: 1.5,
136
142
  borderRadius: 1,
137
143
  bgcolor: isPassed ? 'action.hover' : 'transparent',
138
144
  border: isPassed ? 'none' : `1px solid ${theme.palette.divider}`,
139
145
  }}>
140
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
146
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexShrink: 0 }}>
141
147
  <Chip
142
148
  label={checkResult.result}
143
149
  size='small'
@@ -162,12 +168,51 @@ const CheckResultItem: FC<CheckResultItemProps> = ({ checkResult }) => {
162
168
  sx={{ fontSize: '0.65rem', height: 18 }}
163
169
  />
164
170
  </Box>
165
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, color: 'text.secondary' }}>
166
- {checkResult.summary && (
171
+ {showSummaryInMiddle && (
172
+ <Typography
173
+ variant='caption'
174
+ sx={{
175
+ flex: 1,
176
+ minWidth: 0,
177
+ lineHeight: 1.5,
178
+ color: 'text.secondary',
179
+ }}
180
+ >
181
+ {checkResult.summary}
182
+ </Typography>
183
+ )}
184
+ <Box sx={{
185
+ display: 'flex',
186
+ alignItems: 'center',
187
+ gap: 2,
188
+ ml: 'auto',
189
+ color: 'text.secondary',
190
+ flexShrink: 0,
191
+ }}>
192
+ {!showSummaryInMiddle && checkResult.summary && (
167
193
  <Typography variant='caption'>
168
194
  {checkResult.summary}
169
195
  </Typography>
170
196
  )}
197
+ {checkResult.externalUrl && (
198
+ <Link
199
+ href={checkResult.externalUrl}
200
+ target='_blank'
201
+ rel='noopener noreferrer'
202
+ onClick={(e) => e.stopPropagation()}
203
+ sx={{
204
+ display: 'inline-flex',
205
+ alignItems: 'center',
206
+ gap: 0.25,
207
+ fontSize: '0.7rem',
208
+ fontWeight: 500,
209
+ whiteSpace: 'nowrap',
210
+ }}
211
+ >
212
+ View in scanner
213
+ <OpenInNewIcon sx={{ fontSize: '0.85rem' }} />
214
+ </Link>
215
+ )}
171
216
  {checkResult.durationMs !== null && (
172
217
  <Typography variant='caption' sx={{ minWidth: 50, textAlign: 'right' }}>
173
218
  {formatDuration(checkResult.durationMs)}
@@ -8,42 +8,99 @@
8
8
  * SPDX-License-Identifier: EPL-2.0
9
9
  ********************************************************************************/
10
10
 
11
- import { FunctionComponent, PropsWithChildren, ReactNode, useState } from 'react';
12
- import { ListItemButton, ListItemText, Collapse, List, ListItemIcon } from '@mui/material';
11
+ import { FunctionComponent, PropsWithChildren, ReactNode, useContext, useRef, useState } from 'react';
12
+ import {
13
+ Collapse, List, ListItemButton, ListItemIcon, ListItemText,
14
+ Popover, Tooltip,
15
+ } from '@mui/material';
13
16
  import ExpandLess from '@mui/icons-material/ExpandLess';
14
17
  import ExpandMore from '@mui/icons-material/ExpandMore';
15
18
  import { useNavigate } from 'react-router';
19
+ import { SidebarContext } from './sidebar-context';
20
+
21
+ const EXPANDED_CONTEXT = { collapsed: false };
16
22
 
17
23
  export const NavigationItem: FunctionComponent<PropsWithChildren<NavigationProps>> = props => {
18
- const [open, setOpen] = useState(false);
24
+ const [groupExpanded, setGroupExpanded] = useState(false);
25
+ const [popoverOpen, setPopoverOpen] = useState(false);
26
+ const anchorRef = useRef<HTMLDivElement>(null);
27
+ const { collapsed } = useContext(SidebarContext);
19
28
  const navigate = useNavigate();
20
29
 
30
+ const isGroup = !!props.children;
31
+
21
32
  const handleClick = () => {
22
- if (props.children) {
23
- setOpen(!open);
33
+ if (isGroup) {
34
+ if (collapsed) {
35
+ setPopoverOpen(true);
36
+ } else {
37
+ setGroupExpanded(prev => !prev);
38
+ }
24
39
  } else if (props.route) {
25
- props.onOpenRoute?.call(this, props.route);
26
40
  navigate(props.route);
27
41
  }
28
42
  };
29
43
 
30
- return (<>
31
- <ListItemButton sx={ props.active ? { bgcolor: 'action.selected' } : null } onClick={handleClick}>
32
- {
33
- props.icon && <ListItemIcon>{props.icon}</ListItemIcon>
34
- }
35
- <ListItemText primary={props.label} />
36
- {props.children && (open ? <ExpandLess /> : <ExpandMore />)}
44
+ const button = (
45
+ <ListItemButton
46
+ ref={anchorRef}
47
+ selected={props.active}
48
+ onClick={handleClick}
49
+ sx={{
50
+ minHeight: 48,
51
+ px: 2.5,
52
+ justifyContent: collapsed ? 'center' : 'initial',
53
+ }}
54
+ >
55
+ {props.icon && (
56
+ <ListItemIcon sx={{ minWidth: 0, mr: collapsed ? 'auto' : 3, justifyContent: 'center' }}>
57
+ {props.icon}
58
+ </ListItemIcon>
59
+ )}
60
+ {!collapsed && <ListItemText primary={props.label} />}
61
+ {!collapsed && isGroup && (groupExpanded ? <ExpandLess /> : <ExpandMore />)}
37
62
  </ListItemButton>
38
- {
39
- props.children &&
40
- <Collapse in={open} timeout='auto' unmountOnExit>
41
- <List sx={{ pl: 4 }}>
42
- {props.children}
43
- </List>
44
- </Collapse>
45
- }
46
- </>);
63
+ );
64
+
65
+ return (
66
+ <>
67
+ {collapsed ? (
68
+ <Tooltip title={props.label} placement='right'>
69
+ {button}
70
+ </Tooltip>
71
+ ) : (
72
+ button
73
+ )}
74
+
75
+ {/* Inline expand for groups when sidebar is open */}
76
+ {!collapsed && isGroup && (
77
+ <Collapse in={groupExpanded} timeout='auto' unmountOnExit>
78
+ <List sx={{ pl: 2 }} disablePadding>
79
+ {props.children}
80
+ </List>
81
+ </Collapse>
82
+ )}
83
+
84
+ {/* Floating popover for groups when sidebar is collapsed */}
85
+ {isGroup && (
86
+ <Popover
87
+ open={popoverOpen}
88
+ anchorEl={anchorRef.current}
89
+ onClose={() => setPopoverOpen(false)}
90
+ anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
91
+ transformOrigin={{ vertical: 'top', horizontal: 'left' }}
92
+ disableRestoreFocus
93
+ elevation={2}
94
+ >
95
+ <SidebarContext.Provider value={EXPANDED_CONTEXT}>
96
+ <List disablePadding onClick={() => setPopoverOpen(false)}>
97
+ {props.children}
98
+ </List>
99
+ </SidebarContext.Provider>
100
+ </Popover>
101
+ )}
102
+ </>
103
+ );
47
104
  };
48
105
 
49
106
  export interface NavigationProps {
@@ -51,5 +108,4 @@ export interface NavigationProps {
51
108
  icon?: ReactNode;
52
109
  label: string;
53
110
  active?: boolean;
54
- onOpenRoute?: (route: string) => void;
55
111
  }
@@ -0,0 +1,17 @@
1
+ /******************************************************************************
2
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
3
+ *
4
+ * See the NOTICE file(s) distributed with this work for additional
5
+ * information regarding copyright ownership.
6
+ *
7
+ * This program and the accompanying materials are made available under the
8
+ * terms of the Eclipse Public License 2.0 which is available at
9
+ * https://www.eclipse.org/legal/epl-2.0.
10
+ *
11
+ * SPDX-License-Identifier: EPL-2.0
12
+ *****************************************************************************/
13
+
14
+ import { createContext } from 'react';
15
+
16
+
17
+ export const SidebarContext = createContext<{ collapsed: boolean; }>({ collapsed: false });
@@ -8,43 +8,71 @@
8
8
  * SPDX-License-Identifier: EPL-2.0
9
9
  ********************************************************************************/
10
10
 
11
- import { FunctionComponent, PropsWithChildren } from 'react';
11
+ import { FunctionComponent, PropsWithChildren, useMemo } from 'react';
12
12
  import { Divider, Drawer, IconButton, List } from '@mui/material';
13
+ import { styled, Theme, CSSObject } from '@mui/material/styles';
13
14
  import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
15
+ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
14
16
  import { DrawerHeader } from './drawer-header';
17
+ import { SidebarContext } from './sidebar-context';
15
18
 
16
- export const Sidepanel: FunctionComponent<PropsWithChildren<SidepanelProps>> = props => {
17
- const width = props.width;
19
+ export const DRAWER_WIDTH = 240;
20
+ export const COLLAPSED_WIDTH = 57;
18
21
 
22
+ const openedMixin = (theme: Theme): CSSObject => ({
23
+ width: DRAWER_WIDTH,
24
+ transition: theme.transitions.create('width', {
25
+ easing: theme.transitions.easing.sharp,
26
+ duration: theme.transitions.duration.enteringScreen,
27
+ }),
28
+ overflowX: 'hidden',
29
+ });
30
+
31
+ const closedMixin = (theme: Theme): CSSObject => ({
32
+ width: COLLAPSED_WIDTH,
33
+ transition: theme.transitions.create('width', {
34
+ easing: theme.transitions.easing.sharp,
35
+ duration: theme.transitions.duration.leavingScreen,
36
+ }),
37
+ overflowX: 'hidden',
38
+ });
39
+
40
+ const StyledDrawer = styled(Drawer, { shouldForwardProp: (prop) => prop !== 'open' })(
41
+ ({ theme, open }) => ({
42
+ width: DRAWER_WIDTH,
43
+ flexShrink: 0,
44
+ whiteSpace: 'nowrap',
45
+ boxSizing: 'border-box',
46
+ ...(open ? {
47
+ ...openedMixin(theme),
48
+ '& .MuiDrawer-paper': openedMixin(theme),
49
+ } : {
50
+ ...closedMixin(theme),
51
+ '& .MuiDrawer-paper': closedMixin(theme),
52
+ }),
53
+ })
54
+ );
55
+
56
+ export const Sidepanel: FunctionComponent<PropsWithChildren<SidepanelProps>> = ({ open, onToggle, children }) => {
57
+ const contextValue = useMemo(() => ({ collapsed: !open }), [open]);
19
58
  return (
20
- <Drawer
21
- sx={{
22
- width: width,
23
- flexShrink: 0,
24
- '& .MuiDrawer-paper': {
25
- width: width,
26
- boxSizing: 'border-box',
27
- },
28
- }}
29
- variant='persistent'
30
- anchor='left'
31
- open={props.open}
32
- >
33
- <DrawerHeader>
34
- <IconButton onClick={props.handleDrawerClose}>
35
- <ChevronLeftIcon />
36
- </IconButton>
37
- </DrawerHeader>
38
- <Divider />
39
- <List>
40
- {props.children}
41
- </List>
42
- </Drawer>
59
+ <SidebarContext.Provider value={contextValue}>
60
+ <StyledDrawer variant='permanent' anchor='left' open={open}>
61
+ <DrawerHeader>
62
+ <IconButton onClick={onToggle} aria-label={open ? 'collapse sidebar' : 'expand sidebar'}>
63
+ {open ? <ChevronLeftIcon /> : <ChevronRightIcon />}
64
+ </IconButton>
65
+ </DrawerHeader>
66
+ <Divider />
67
+ <List disablePadding>
68
+ {children}
69
+ </List>
70
+ </StyledDrawer>
71
+ </SidebarContext.Provider>
43
72
  );
44
73
  };
45
74
 
46
- interface SidepanelProps {
47
- width: number;
75
+ export interface SidepanelProps {
48
76
  open: boolean;
49
- handleDrawerClose: () => void;
77
+ onToggle: () => void;
50
78
  }
@@ -211,6 +211,7 @@ export const useScansEffect = (
211
211
  summary: result.summary || null,
212
212
  errorMessage: result.errorMessage || null,
213
213
  required: result.required ?? null,
214
+ externalUrl: result.externalUrl || null,
214
215
  })),
215
216
  extensionIcon: scan.extensionIcon,
216
217
  downloadUrl: scan.downloadUrl || null,
@@ -58,6 +58,8 @@ export interface CheckResult {
58
58
  errorMessage: string | null;
59
59
  /** Whether this check was required (errors block publishing). Null for scanner jobs. */
60
60
  required: boolean | null;
61
+ /** Deep link to the external scanner's dashboard for this job. Null if not applicable. */
62
+ externalUrl: string | null;
61
63
  }
62
64
 
63
65
  export interface ScanResult {
@@ -504,6 +504,7 @@ export interface AdminService {
504
504
  deleteExtensions(abortController: AbortController, req: { namespace: string, extension: string, targetPlatformVersions?: object[] }): Promise<Readonly<SuccessResult | ErrorResult>>
505
505
  getNamespace(abortController: AbortController, name: string): Promise<Readonly<Namespace>>
506
506
  createNamespace(abortController: AbortController, namespace: { name: string }): Promise<Readonly<SuccessResult | ErrorResult>>
507
+ deleteNamespace(abortController: AbortController, namespace: { name: string }): Promise<Readonly<SuccessResult | ErrorResult>>
507
508
  changeNamespace(abortController: AbortController, req: {oldNamespace: string, newNamespace: string, removeOldNamespace: boolean, mergeIfNewNamespaceAlreadyExists: boolean}): Promise<Readonly<SuccessResult | ErrorResult>>
508
509
  getPublisherInfo(abortController: AbortController, provider: string, login: string): Promise<Readonly<PublisherInfo>>
509
510
  revokePublisherContributions(abortController: AbortController, provider: string, login: string): Promise<Readonly<SuccessResult | ErrorResult>>
@@ -601,6 +602,23 @@ export class AdminServiceImpl implements AdminService {
601
602
  });
602
603
  }
603
604
 
605
+ async deleteNamespace(abortController: AbortController, namespace: { name: string }): Promise<Readonly<SuccessResult | ErrorResult>> {
606
+ const csrfResponse = await this.registry.getCsrfToken(abortController);
607
+ const headers: Record<string, string> = {
608
+ 'Content-Type': 'application/json;charset=UTF-8'
609
+ };
610
+ if (!isError(csrfResponse)) {
611
+ const csrfToken = csrfResponse as CsrfTokenJson;
612
+ headers[csrfToken.header] = csrfToken.value;
613
+ }
614
+ return sendRequest({
615
+ abortController,
616
+ credentials: true,
617
+ endpoint: createAbsoluteURL([this.registry.serverUrl, 'admin', 'namespace', namespace.name]),
618
+ method: 'DELETE',
619
+ headers
620
+ });
621
+ }
604
622
  async changeNamespace(abortController: AbortController, req: {oldNamespace: string, newNamespace: string, removeOldNamespace: boolean, mergeIfNewNamespaceAlreadyExists: boolean}): Promise<Readonly<SuccessResult | ErrorResult>> {
605
623
  const csrfResponse = await this.registry.getCsrfToken(abortController);
606
624
  const headers: Record<string, string> = {
@@ -0,0 +1,67 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
3
+ *
4
+ * See the NOTICE file(s) distributed with this work for additional
5
+ * information regarding copyright ownership.
6
+ *
7
+ * This program and the accompanying materials are made available under the
8
+ * terms of the Eclipse Public License 2.0 which is available at
9
+ * https://www.eclipse.org/legal/epl-2.0.
10
+ *
11
+ * SPDX-License-Identifier: EPL-2.0
12
+ *****************************************************************************/
13
+
14
+ import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
15
+
16
+ /**
17
+ * Reads a value from localStorage and deserializes it.
18
+ * Returns `fallback` when the key is missing or the stored value cannot be parsed.
19
+ */
20
+ function readStorage<T>(key: string, fallback: T): T {
21
+ try {
22
+ const raw = localStorage.getItem(key);
23
+ return raw === null ? fallback : (JSON.parse(raw) as T);
24
+ } catch {
25
+ return fallback;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * A useState-compatible hook that persists its value in localStorage.
31
+ *
32
+ * - Initialises from localStorage (falling back to `initialValue`).
33
+ * - Keeps multiple tabs in sync by listening to the `storage` event.
34
+ * - The setter accepts both a direct value and an updater function, matching
35
+ * the full useState API.
36
+ */
37
+ export function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>] {
38
+ const [state, setState] = useState<T>(() => readStorage(key, initialValue));
39
+
40
+ // Persist every state change to localStorage.
41
+ useEffect(() => {
42
+ try {
43
+ localStorage.setItem(key, JSON.stringify(state));
44
+ } catch {
45
+ // Quota exceeded or private-browsing restrictions — silently ignore.
46
+ }
47
+ }, [key, state]);
48
+
49
+ // Keep tabs in sync: when another tab writes to the same key, update state.
50
+ useEffect(() => {
51
+ const onStorage = (event: StorageEvent) => {
52
+ if (event.key !== key || event.storageArea !== localStorage) {
53
+ return;
54
+ }
55
+ setState(event.newValue === null ? initialValue : (JSON.parse(event.newValue) as T));
56
+ };
57
+
58
+ globalThis.addEventListener('storage', onStorage);
59
+ return () => globalThis.removeEventListener('storage', onStorage);
60
+ }, [key, initialValue]);
61
+
62
+ const setValue: Dispatch<SetStateAction<T>> = useCallback((action) => {
63
+ setState(prev => (typeof action === 'function' ? (action as (prev: T) => T)(prev) : action));
64
+ }, []);
65
+
66
+ return [state, setValue];
67
+ }