openvsx-webui-test 0.20.0-dev.0 → 0.20.0-dev.2
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.
- package/lib/components/error-dialog.d.ts.map +1 -1
- package/lib/components/error-dialog.js +3 -5
- package/lib/components/error-dialog.js.map +1 -1
- package/lib/components/generate-token-dialog.d.ts +22 -0
- package/lib/components/generate-token-dialog.d.ts.map +1 -0
- package/lib/components/generate-token-dialog.js +91 -0
- package/lib/components/generate-token-dialog.js.map +1 -0
- package/lib/components/rate-limiting/usage-stats/usage-stats-chart.d.ts.map +1 -1
- package/lib/components/rate-limiting/usage-stats/usage-stats-chart.js +14 -11
- package/lib/components/rate-limiting/usage-stats/usage-stats-chart.js.map +1 -1
- package/lib/components/timestamp.d.ts +1 -0
- package/lib/components/timestamp.d.ts.map +1 -1
- package/lib/components/timestamp.js +2 -1
- package/lib/components/timestamp.js.map +1 -1
- package/lib/default/menu-content.d.ts +1 -1
- package/lib/extension-registry-service.d.ts +7 -1
- package/lib/extension-registry-service.d.ts.map +1 -1
- package/lib/extension-registry-service.js +41 -0
- package/lib/extension-registry-service.js.map +1 -1
- package/lib/extension-registry-types.d.ts +6 -0
- package/lib/extension-registry-types.d.ts.map +1 -1
- package/lib/pages/admin-dashboard/admin-dashboard.d.ts.map +1 -1
- package/lib/pages/admin-dashboard/admin-dashboard.js +17 -27
- package/lib/pages/admin-dashboard/admin-dashboard.js.map +1 -1
- package/lib/pages/admin-dashboard/customers/customer-details.d.ts.map +1 -1
- package/lib/pages/admin-dashboard/customers/customer-details.js +2 -1
- package/lib/pages/admin-dashboard/customers/customer-details.js.map +1 -1
- package/lib/pages/admin-dashboard/customers/customer-token-list.d.ts +19 -0
- package/lib/pages/admin-dashboard/customers/customer-token-list.d.ts.map +1 -0
- package/lib/pages/admin-dashboard/customers/customer-token-list.js +70 -0
- package/lib/pages/admin-dashboard/customers/customer-token-list.js.map +1 -0
- package/lib/pages/extension-detail/extension-detail.js +1 -1
- package/lib/pages/extension-detail/extension-detail.js.map +1 -1
- package/lib/pages/user/{generate-token-dialog.d.ts → generate-access-token-dialog.d.ts} +2 -2
- package/lib/pages/user/generate-access-token-dialog.d.ts.map +1 -0
- package/lib/pages/user/generate-access-token-dialog.js +33 -0
- package/lib/pages/user/generate-access-token-dialog.js.map +1 -0
- package/lib/pages/user/user-settings-profile.d.ts.map +1 -1
- package/lib/pages/user/user-settings-profile.js +1 -1
- package/lib/pages/user/user-settings-profile.js.map +1 -1
- package/lib/pages/user/user-settings-tokens.js +4 -4
- package/lib/pages/user/user-settings-tokens.js.map +1 -1
- package/lib/utils.d.ts +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +10 -5
- package/lib/utils.js.map +1 -1
- package/package.json +9 -9
- package/src/components/error-dialog.tsx +3 -5
- package/src/components/generate-token-dialog.tsx +167 -0
- package/src/components/rate-limiting/usage-stats/usage-stats-chart.tsx +18 -12
- package/src/components/timestamp.tsx +3 -1
- package/src/extension-registry-service.ts +49 -1
- package/src/extension-registry-types.ts +7 -0
- package/src/pages/admin-dashboard/admin-dashboard.tsx +17 -27
- package/src/pages/admin-dashboard/customers/customer-details.tsx +4 -0
- package/src/pages/admin-dashboard/customers/customer-token-list.tsx +135 -0
- package/src/pages/extension-detail/extension-detail.tsx +1 -1
- package/src/pages/user/generate-access-token-dialog.tsx +49 -0
- package/src/pages/user/user-settings-profile.tsx +1 -1
- package/src/pages/user/user-settings-tokens.tsx +4 -4
- package/src/utils.ts +9 -5
- package/lib/components/copy-to-clipboard.d.ts +0 -21
- package/lib/components/copy-to-clipboard.d.ts.map +0 -1
- package/lib/components/copy-to-clipboard.js +0 -25
- package/lib/components/copy-to-clipboard.js.map +0 -1
- package/lib/pages/user/generate-token-dialog.d.ts.map +0 -1
- package/lib/pages/user/generate-token-dialog.js +0 -79
- package/lib/pages/user/generate-token-dialog.js.map +0 -1
- package/src/components/copy-to-clipboard.tsx +0 -50
- package/src/pages/user/generate-token-dialog.tsx +0 -157
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
LoginProviders, ScanResultJson, ScanCounts, ScanResultsResponse, ScanFilterOptions,
|
|
16
16
|
FilesResponse, FileDecisionCountsJson, ScanDecisionRequest, ScanDecisionResponse,
|
|
17
17
|
FileDecisionRequest, FileDecisionResponse, FileDecisionDeleteRequest, FileDecisionDeleteResponse,
|
|
18
|
-
Tier, TierList, Customer, CustomerList, UsageStatsList, LogPageableList, CustomerMembershipList,
|
|
18
|
+
Tier, TierList, Customer, CustomerList, UsageStatsList, LogPageableList, CustomerMembershipList, RateLimitToken,
|
|
19
19
|
} from './extension-registry-types';
|
|
20
20
|
import { createAbsoluteURL, addQuery } from './utils';
|
|
21
21
|
import { sendRequest, ErrorResponse } from './server-request';
|
|
@@ -533,6 +533,9 @@ export interface AdminService {
|
|
|
533
533
|
removeCustomerMember(abortController: AbortController, name: string, user: UserData): Promise<Readonly<SuccessResult | ErrorResult>>;
|
|
534
534
|
getUsageStats(abortController: AbortController, customerName: string, date: Date): Promise<Readonly<UsageStatsList>>;
|
|
535
535
|
getLogs(abortController: AbortController, page?: number, size?: number, period?: string): Promise<Readonly<LogPageableList>>;
|
|
536
|
+
getCustomerRateLimitTokens(abortController: AbortController, customerName: string): Promise<Readonly<RateLimitToken[]>>;
|
|
537
|
+
createCustomerRateLimitToken(abortController: AbortController, customerName: string, description: string): Promise<Readonly<RateLimitToken>>;
|
|
538
|
+
deleteCustomerRateLimitToken(abortController: AbortController, customerName: string, tokenId: number): Promise<Readonly<SuccessResult | ErrorResult>>;
|
|
536
539
|
}
|
|
537
540
|
|
|
538
541
|
export interface AdminServiceConstructor {
|
|
@@ -1065,6 +1068,51 @@ export class AdminServiceImpl implements AdminService {
|
|
|
1065
1068
|
credentials: true
|
|
1066
1069
|
}, false);
|
|
1067
1070
|
}
|
|
1071
|
+
|
|
1072
|
+
async getCustomerRateLimitTokens(abortController: AbortController, customerName: string): Promise<Readonly<RateLimitToken[]>> {
|
|
1073
|
+
return sendRequest({
|
|
1074
|
+
abortController,
|
|
1075
|
+
credentials: true,
|
|
1076
|
+
endpoint: createAbsoluteURL([this.registry.serverUrl, 'admin', 'ratelimit', 'customers', customerName, 'tokens']),
|
|
1077
|
+
}, false);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async createCustomerRateLimitToken(abortController: AbortController, customerName: string, description?: string): Promise<Readonly<RateLimitToken>> {
|
|
1081
|
+
const csrfResponse = await this.registry.getCsrfToken(abortController);
|
|
1082
|
+
const headers: Record<string, string> = {};
|
|
1083
|
+
if (!isError(csrfResponse)) {
|
|
1084
|
+
const csrfToken = csrfResponse as CsrfTokenJson;
|
|
1085
|
+
headers[csrfToken.header] = csrfToken.value;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const url = createAbsoluteURL([this.registry.serverUrl, 'admin', 'ratelimit', 'customers', customerName, 'tokens']);
|
|
1089
|
+
const endpoint = description !== undefined ? addQuery(url, [{ key: 'description', value: description }]) : url;
|
|
1090
|
+
return sendRequest({
|
|
1091
|
+
abortController,
|
|
1092
|
+
method: 'POST',
|
|
1093
|
+
credentials: true,
|
|
1094
|
+
endpoint,
|
|
1095
|
+
headers
|
|
1096
|
+
}, false);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async deleteCustomerRateLimitToken(abortController: AbortController, customerName: string, tokenId: number): Promise<Readonly<SuccessResult | ErrorResult>> {
|
|
1100
|
+
const csrfResponse = await this.registry.getCsrfToken(abortController);
|
|
1101
|
+
const headers: Record<string, string> = {
|
|
1102
|
+
'Content-Type': 'application/json;charset=UTF-8'
|
|
1103
|
+
};
|
|
1104
|
+
if (!isError(csrfResponse)) {
|
|
1105
|
+
const csrfToken = csrfResponse as CsrfTokenJson;
|
|
1106
|
+
headers[csrfToken.header] = csrfToken.value;
|
|
1107
|
+
}
|
|
1108
|
+
return sendRequest({
|
|
1109
|
+
abortController,
|
|
1110
|
+
method: 'DELETE',
|
|
1111
|
+
credentials: true,
|
|
1112
|
+
endpoint: createAbsoluteURL([this.registry.serverUrl, 'admin', 'ratelimit', 'customers', customerName, 'tokens', `${tokenId}`]),
|
|
1113
|
+
headers
|
|
1114
|
+
}, false);
|
|
1115
|
+
}
|
|
1068
1116
|
}
|
|
1069
1117
|
|
|
1070
1118
|
export interface ExtensionFilter {
|
|
@@ -467,6 +467,13 @@ export interface CustomerMembershipList {
|
|
|
467
467
|
customerMemberships: CustomerMembership[];
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
export interface RateLimitToken {
|
|
471
|
+
id: number;
|
|
472
|
+
value?: string;
|
|
473
|
+
description?: string;
|
|
474
|
+
createdTimestamp: TimestampString;
|
|
475
|
+
}
|
|
476
|
+
|
|
470
477
|
export interface UsageStats {
|
|
471
478
|
windowStart: number; // epoch seconds in UTC
|
|
472
479
|
duration: number; // in seconds
|
|
@@ -119,24 +119,19 @@ interface AppBarProps extends MuiAppBarProps {
|
|
|
119
119
|
|
|
120
120
|
const AppBar = styled(MuiAppBar, {
|
|
121
121
|
shouldForwardProp: (prop) => prop !== 'open',
|
|
122
|
-
})<AppBarProps>(({ theme }) => ({
|
|
122
|
+
})<AppBarProps>(({ theme, open }) => ({
|
|
123
123
|
transition: theme.transitions.create(['margin', 'width'], {
|
|
124
124
|
easing: theme.transitions.easing.sharp,
|
|
125
125
|
duration: theme.transitions.duration.leavingScreen,
|
|
126
126
|
}),
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
duration: theme.transitions.duration.enteringScreen,
|
|
136
|
-
}),
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
],
|
|
127
|
+
...(open && {
|
|
128
|
+
width: `calc(100% - ${drawerWidth}px)`,
|
|
129
|
+
marginLeft: `${drawerWidth}px`,
|
|
130
|
+
transition: theme.transitions.create(['margin', 'width'], {
|
|
131
|
+
easing: theme.transitions.easing.easeOut,
|
|
132
|
+
duration: theme.transitions.duration.enteringScreen,
|
|
133
|
+
}),
|
|
134
|
+
}),
|
|
140
135
|
}));
|
|
141
136
|
|
|
142
137
|
interface LinkRouterProps extends LinkProps {
|
|
@@ -178,7 +173,7 @@ const BreadcrumbsComponent = () => {
|
|
|
178
173
|
|
|
179
174
|
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{
|
|
180
175
|
open?: boolean;
|
|
181
|
-
}>(({ theme }) => ({
|
|
176
|
+
}>(({ theme, open }) => ({
|
|
182
177
|
flexGrow: 1,
|
|
183
178
|
padding: theme.spacing(3),
|
|
184
179
|
transition: theme.transitions.create('margin', {
|
|
@@ -186,18 +181,13 @@ const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{
|
|
|
186
181
|
duration: theme.transitions.duration.leavingScreen,
|
|
187
182
|
}),
|
|
188
183
|
marginLeft: `-${drawerWidth}px`,
|
|
189
|
-
|
|
190
|
-
{
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}),
|
|
197
|
-
marginLeft: 0,
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
],
|
|
184
|
+
...(open && {
|
|
185
|
+
transition: theme.transitions.create('margin', {
|
|
186
|
+
easing: theme.transitions.easing.easeOut,
|
|
187
|
+
duration: theme.transitions.duration.enteringScreen,
|
|
188
|
+
}),
|
|
189
|
+
marginLeft: 0,
|
|
190
|
+
}),
|
|
201
191
|
}));
|
|
202
192
|
|
|
203
193
|
export const AdminDashboard: FunctionComponent<AdminDashboardProps> = props => {
|
|
@@ -28,6 +28,7 @@ import { useAdminUsageStats } from '../usage-stats/use-usage-stats';
|
|
|
28
28
|
import { GeneralDetails, UsageStats } from '../../../components/rate-limiting/customer';
|
|
29
29
|
import { CustomerFormDialog } from './customer-form-dialog';
|
|
30
30
|
import { CustomerMemberList } from './customer-member-list';
|
|
31
|
+
import { CustomerTokenList } from './customer-token-list';
|
|
31
32
|
|
|
32
33
|
const CustomerDetailsLoading: FC = () => (
|
|
33
34
|
<Box sx={{ p: 3 }}>
|
|
@@ -115,6 +116,9 @@ export const CustomerDetails: FC = () => {
|
|
|
115
116
|
<CustomerMemberList
|
|
116
117
|
customer={customer}
|
|
117
118
|
/>
|
|
119
|
+
<CustomerTokenList
|
|
120
|
+
customer={customer}
|
|
121
|
+
/>
|
|
118
122
|
<UsageStats usageStats={usageStats} customer={customer} startDate={startDate} onStartDateChange={setStartDate} />
|
|
119
123
|
</Box>
|
|
120
124
|
|
|
@@ -0,0 +1,135 @@
|
|
|
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 { FunctionComponent, useEffect, useState, useContext, useRef } from 'react';
|
|
15
|
+
import {
|
|
16
|
+
Box,
|
|
17
|
+
Typography,
|
|
18
|
+
Divider,
|
|
19
|
+
List,
|
|
20
|
+
ListItem,
|
|
21
|
+
ListItemText,
|
|
22
|
+
IconButton,
|
|
23
|
+
Paper,
|
|
24
|
+
Button,
|
|
25
|
+
type PaperProps
|
|
26
|
+
} from '@mui/material';
|
|
27
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
28
|
+
import AddIcon from '@mui/icons-material/Add';
|
|
29
|
+
import { MainContext } from '../../../context';
|
|
30
|
+
import { Customer, isError, RateLimitToken } from '../../../extension-registry-types';
|
|
31
|
+
import { GenerateTokenDialog } from '../../../components/generate-token-dialog';
|
|
32
|
+
import { Timestamp } from "../../../components/timestamp";
|
|
33
|
+
|
|
34
|
+
const sectionPaperProps: PaperProps = { elevation: 1, sx: { p: 3, mb: 3 } };
|
|
35
|
+
|
|
36
|
+
export const CustomerTokenList: FunctionComponent<CustomerTokenListProps> = props => {
|
|
37
|
+
const { service, handleError } = useContext(MainContext);
|
|
38
|
+
const [tokens, setTokens] = useState<RateLimitToken[]>([]);
|
|
39
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
40
|
+
const abortController = useRef<AbortController>(new AbortController());
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
fetchTokens();
|
|
44
|
+
}, [props.customer]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
return () => {
|
|
48
|
+
abortController.current.abort();
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const fetchTokens = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const result = await service.admin.getCustomerRateLimitTokens(abortController.current, props.customer.name);
|
|
55
|
+
setTokens([...result]);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
handleError(err);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleGenerate = async (description: string): Promise<string> => {
|
|
62
|
+
const token = await service.admin.createCustomerRateLimitToken(abortController.current, props.customer.name, description);
|
|
63
|
+
await fetchTokens();
|
|
64
|
+
return token.value ?? '';
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleDelete = async (tokenId: number) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await service.admin.deleteCustomerRateLimitToken(abortController.current, props.customer.name, tokenId);
|
|
70
|
+
if (isError(result)) {
|
|
71
|
+
throw result;
|
|
72
|
+
}
|
|
73
|
+
await fetchTokens();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
handleError(err);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return <Paper {...sectionPaperProps}>
|
|
80
|
+
<Box
|
|
81
|
+
sx={{
|
|
82
|
+
display: 'flex',
|
|
83
|
+
justifyContent: 'space-between',
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
mb: 1,
|
|
86
|
+
flexDirection: { xs: 'column', sm: 'column', md: 'row', lg: 'row', xl: 'row' }
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<Typography variant='h6'>Tokens</Typography>
|
|
90
|
+
<Button size='small' startIcon={<AddIcon />} onClick={() => setDialogOpen(true)}>
|
|
91
|
+
Generate Token
|
|
92
|
+
</Button>
|
|
93
|
+
</Box>
|
|
94
|
+
<Divider sx={{ mb: 1 }} />
|
|
95
|
+
{tokens.length === 0 ? (
|
|
96
|
+
<Typography variant='body2' color='text.secondary' sx={{ py: 1 }}>
|
|
97
|
+
No rate limiting tokens for this customer.
|
|
98
|
+
</Typography>
|
|
99
|
+
) : (
|
|
100
|
+
<List dense disablePadding>
|
|
101
|
+
{tokens.map(token => (
|
|
102
|
+
<ListItem
|
|
103
|
+
key={token.id}
|
|
104
|
+
secondaryAction={
|
|
105
|
+
<IconButton edge='end' size='small' color='error' onClick={() => handleDelete(token.id)} title='Delete token'>
|
|
106
|
+
<DeleteIcon fontSize='small' />
|
|
107
|
+
</IconButton>
|
|
108
|
+
}
|
|
109
|
+
>
|
|
110
|
+
<ListItemText
|
|
111
|
+
primary={
|
|
112
|
+
<Typography sx={{ fontWeight: 'bold', overflow: 'hidden', textOverflow: 'ellipsis' }}>{token.description || 'N/A'}</Typography>
|
|
113
|
+
}
|
|
114
|
+
secondary={
|
|
115
|
+
<Typography variant='body2'>Created: <Timestamp value={token.createdTimestamp}/></Typography>
|
|
116
|
+
}
|
|
117
|
+
/>
|
|
118
|
+
</ListItem>
|
|
119
|
+
))}
|
|
120
|
+
</List>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<GenerateTokenDialog
|
|
124
|
+
open={dialogOpen}
|
|
125
|
+
onClose={() => setDialogOpen(false)}
|
|
126
|
+
onGenerate={handleGenerate}
|
|
127
|
+
onError={handleError}
|
|
128
|
+
title='Generate token'
|
|
129
|
+
/>
|
|
130
|
+
</Paper>;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export interface CustomerTokenListProps {
|
|
134
|
+
customer: Customer;
|
|
135
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
Typography, Box, Theme, Container, Link, Avatar, Paper, Badge, SxProps, Tabs, Tab, Stack, useTheme, PaletteMode,
|
|
14
14
|
decomposeColor
|
|
15
15
|
} from '@mui/material';
|
|
16
|
+
import { styled } from '@mui/material/styles';
|
|
16
17
|
import { Link as RouteLink, useNavigate, useParams } from 'react-router-dom';
|
|
17
18
|
import SaveAltIcon from '@mui/icons-material/SaveAlt';
|
|
18
19
|
import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
|
|
@@ -29,7 +30,6 @@ import { ExtensionDetailOverview } from './extension-detail-overview';
|
|
|
29
30
|
import { ExtensionDetailChanges } from './extension-detail-changes';
|
|
30
31
|
import { ExtensionDetailReviews } from './extension-detail-reviews';
|
|
31
32
|
import { ExtensionDetailRoutes } from './extension-detail-routes';
|
|
32
|
-
import styled from '@mui/material/styles/styled';
|
|
33
33
|
|
|
34
34
|
const alignVertically = {
|
|
35
35
|
display: 'flex',
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2019 TypeFox and others
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
* http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
*
|
|
8
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
9
|
+
********************************************************************************/
|
|
10
|
+
|
|
11
|
+
import { FunctionComponent, useContext, useRef, useState } from 'react';
|
|
12
|
+
import { Button } from '@mui/material';
|
|
13
|
+
import { GenerateTokenDialog } from '../../components/generate-token-dialog';
|
|
14
|
+
import { isError } from '../../extension-registry-types';
|
|
15
|
+
import { MainContext } from '../../context';
|
|
16
|
+
|
|
17
|
+
export const GenerateAccessTokenDialog: FunctionComponent<GenerateTokenDialogProps> = props => {
|
|
18
|
+
const context = useContext(MainContext);
|
|
19
|
+
const abortController = useRef<AbortController>(new AbortController());
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
|
|
22
|
+
const handleGenerate = async (description: string): Promise<string> => {
|
|
23
|
+
if (!context.user) {
|
|
24
|
+
throw new Error('Not logged in');
|
|
25
|
+
}
|
|
26
|
+
const token = await context.service.createAccessToken(abortController.current, context.user, description);
|
|
27
|
+
if (isError(token)) {
|
|
28
|
+
throw token;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
props.handleTokenGenerated();
|
|
32
|
+
|
|
33
|
+
return token.value!;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return <>
|
|
37
|
+
<Button variant='outlined' onClick={() => setOpen(true)}>Generate new token</Button>
|
|
38
|
+
<GenerateTokenDialog
|
|
39
|
+
open={open}
|
|
40
|
+
onClose={() => setOpen(false)}
|
|
41
|
+
onGenerate={handleGenerate}
|
|
42
|
+
onError={context.handleError}
|
|
43
|
+
/>
|
|
44
|
+
</>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface GenerateTokenDialogProps {
|
|
48
|
+
handleTokenGenerated: () => void;
|
|
49
|
+
}
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import { FunctionComponent, ReactNode, useContext } from 'react';
|
|
12
12
|
import { Theme, Grid, Typography, Avatar } from '@mui/material';
|
|
13
|
+
import { styled } from '@mui/material/styles';
|
|
13
14
|
import { toLocalTime } from '../../utils';
|
|
14
15
|
import { UserData } from '../../extension-registry-types';
|
|
15
16
|
import { UserPublisherAgreement } from './user-publisher-agreement';
|
|
16
|
-
import styled from '@mui/material/styles/styled';
|
|
17
17
|
import { MainContext } from "../../context";
|
|
18
18
|
|
|
19
19
|
const ProfileGrid = styled(Grid)(({ theme }: {theme: Theme}) => ({
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
import { FunctionComponent, ReactNode, useContext, useEffect, useState, useRef } from 'react';
|
|
12
12
|
import { Theme, Typography, Box, Paper, Button, Link, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions } from '@mui/material';
|
|
13
|
+
import { styled } from '@mui/material/styles';
|
|
13
14
|
import { Link as RouteLink } from 'react-router-dom';
|
|
14
15
|
import { DelayedLoadIndicator } from '../../components/delayed-load-indicator';
|
|
15
16
|
import { Timestamp } from '../../components/timestamp';
|
|
16
17
|
import { PersonalAccessToken } from '../../extension-registry-types';
|
|
17
18
|
import { MainContext } from '../../context';
|
|
18
|
-
import {
|
|
19
|
+
import { GenerateAccessTokenDialog } from './generate-access-token-dialog';
|
|
19
20
|
import { UserSettingsRoutes } from './user-settings-routes';
|
|
20
|
-
import styled from '@mui/material/styles/styled';
|
|
21
21
|
|
|
22
22
|
const link = ({ theme }: { theme: Theme }) => ({
|
|
23
23
|
color: theme.palette.secondary.main,
|
|
@@ -105,7 +105,7 @@ export const UserSettingsTokens: FunctionComponent = () => {
|
|
|
105
105
|
<Box alignItems='center' overflow='auto'>
|
|
106
106
|
<Typography sx={{ fontWeight: 'bold', overflow: 'hidden', textOverflow: 'ellipsis' }}>{token.description}</Typography>
|
|
107
107
|
<Typography variant='body2'>Created: <Timestamp value={token.createdTimestamp}/></Typography>
|
|
108
|
-
<Typography variant='body2'>Expires: {token.expiresTimestamp ? <Timestamp value={token.expiresTimestamp}/> : 'never'}</Typography>
|
|
108
|
+
<Typography variant='body2'>Expires: {token.expiresTimestamp ? <Timestamp value={token.expiresTimestamp} isFutureTime={true} /> : 'never'}</Typography>
|
|
109
109
|
<Typography variant='body2'>Accessed: {token.accessedTimestamp ? <Timestamp value={token.accessedTimestamp}/> : 'never'}</Typography>
|
|
110
110
|
</Box>
|
|
111
111
|
<Box display='flex' alignItems='center'>
|
|
@@ -159,7 +159,7 @@ export const UserSettingsTokens: FunctionComponent = () => {
|
|
|
159
159
|
}}
|
|
160
160
|
>
|
|
161
161
|
<Box mr={1} mb={1}>
|
|
162
|
-
<
|
|
162
|
+
<GenerateAccessTokenDialog
|
|
163
163
|
handleTokenGenerated={handleTokenGenerated}
|
|
164
164
|
/>
|
|
165
165
|
</Box>
|
package/src/utils.ts
CHANGED
|
@@ -52,22 +52,26 @@ export function toLocalTime(timestamp?: string): string | undefined {
|
|
|
52
52
|
return new Intl.DateTimeFormat(undefined, options).format(date);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const
|
|
55
|
+
const msPerSecond = 1000;
|
|
56
|
+
const msPerMinute = msPerSecond * 60;
|
|
56
57
|
const msPerHour = msPerMinute * 60;
|
|
57
58
|
const msPerDay = msPerHour * 24;
|
|
58
59
|
const msPerMonth = msPerDay * 30.4;
|
|
59
60
|
const msPerYear = msPerDay * 365;
|
|
60
|
-
export function toRelativeTime(timestamp?: string): string | undefined {
|
|
61
|
+
export function toRelativeTime(timestamp?: string, isFutureTime: boolean = false): string | undefined {
|
|
61
62
|
if (!timestamp) {
|
|
62
63
|
return undefined;
|
|
63
64
|
}
|
|
64
65
|
const date = new Date(timestamp);
|
|
65
66
|
const elapsed = Date.now() - date.getTime();
|
|
66
67
|
|
|
67
|
-
if (
|
|
68
|
+
if (isFutureTime) {
|
|
68
69
|
const remaining = -elapsed;
|
|
69
|
-
if (remaining <
|
|
70
|
-
return
|
|
70
|
+
if (remaining < 0) {
|
|
71
|
+
return "now";
|
|
72
|
+
} else if (remaining < msPerMinute) {
|
|
73
|
+
const value = Math.round(remaining / msPerSecond);
|
|
74
|
+
return `in ${value} second${value !== 1 ? 's' : ''}`;
|
|
71
75
|
} else if (remaining < msPerHour) {
|
|
72
76
|
const value = Math.round(remaining / msPerMinute);
|
|
73
77
|
return `in ${value} minute${value !== 1 ? 's' : ''}`;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/********************************************************************************
|
|
2
|
-
* Copyright (c) 2020 TypeFox and others
|
|
3
|
-
*
|
|
4
|
-
* This program and the accompanying materials are made available under the
|
|
5
|
-
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
* http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
*
|
|
8
|
-
* SPDX-License-Identifier: EPL-2.0
|
|
9
|
-
********************************************************************************/
|
|
10
|
-
import { FunctionComponent, ReactElement } from 'react';
|
|
11
|
-
import { TooltipProps } from '@mui/material';
|
|
12
|
-
export declare const CopyToClipboard: FunctionComponent<CopyToClipboardProps>;
|
|
13
|
-
interface ChildProps {
|
|
14
|
-
copy: (content: any) => void;
|
|
15
|
-
}
|
|
16
|
-
export interface CopyToClipboardProps {
|
|
17
|
-
tooltipProps?: Partial<TooltipProps>;
|
|
18
|
-
children: (props: ChildProps) => ReactElement<any>;
|
|
19
|
-
}
|
|
20
|
-
export {};
|
|
21
|
-
//# sourceMappingURL=copy-to-clipboard.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"copy-to-clipboard.d.ts","sourceRoot":"","sources":["../../src/components/copy-to-clipboard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;kFAQkF;AAElF,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAY,MAAM,OAAO,CAAC;AAElE,OAAO,EAAW,YAAY,EAAE,MAAM,eAAe,CAAC;AAEtD,eAAO,MAAM,eAAe,EAAE,iBAAiB,CAAC,oBAAoB,CA0BnE,CAAC;AAEF,UAAU,UAAU;IAChB,IAAI,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,oBAAoB;IACjC,YAAY,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,YAAY,CAAC,GAAG,CAAC,CAAC;CACtD"}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
/********************************************************************************
|
|
3
|
-
* Copyright (c) 2020 TypeFox and others
|
|
4
|
-
*
|
|
5
|
-
* This program and the accompanying materials are made available under the
|
|
6
|
-
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
-
* http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
-
*
|
|
9
|
-
* SPDX-License-Identifier: EPL-2.0
|
|
10
|
-
********************************************************************************/
|
|
11
|
-
import { useState } from 'react';
|
|
12
|
-
import copy from 'clipboard-copy';
|
|
13
|
-
import { Tooltip } from '@mui/material';
|
|
14
|
-
export const CopyToClipboard = props => {
|
|
15
|
-
const [showTooltip, setTooltip] = useState(false);
|
|
16
|
-
const handleOnTooltipClose = () => {
|
|
17
|
-
setTooltip(false);
|
|
18
|
-
};
|
|
19
|
-
const onCopy = (content) => {
|
|
20
|
-
copy(content);
|
|
21
|
-
setTooltip(true);
|
|
22
|
-
};
|
|
23
|
-
return (_jsx(Tooltip, { open: showTooltip, title: 'Copied to clipboard!', leaveDelay: 1500, onClose: handleOnTooltipClose, disableHoverListener: true, ...props.tooltipProps ?? {}, children: props.children({ copy: onCopy }) }));
|
|
24
|
-
};
|
|
25
|
-
//# sourceMappingURL=copy-to-clipboard.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"copy-to-clipboard.js","sourceRoot":"","sources":["../../src/components/copy-to-clipboard.tsx"],"names":[],"mappings":";AAAA;;;;;;;;kFAQkF;AAElF,OAAO,EAAmC,QAAQ,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,IAAI,MAAM,gBAAgB,CAAC;AAClC,OAAO,EAAE,OAAO,EAAgB,MAAM,eAAe,CAAC;AAEtD,MAAM,CAAC,MAAM,eAAe,GAA4C,KAAK,CAAC,EAAE;IAC5E,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAU,KAAK,CAAC,CAAC;IAE3D,MAAM,oBAAoB,GAAG,GAAG,EAAE;QAC9B,UAAU,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,CAAC,OAAa,EAAE,EAAE;QAC7B,IAAI,CAAC,OAAO,CAAC,CAAC;QACd,UAAU,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC,CAAC;IAEF,OAAO,CACH,KAAC,OAAO,IACJ,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,sBAAsB,EAC7B,UAAU,EAAE,IAAI,EAChB,OAAO,EAAE,oBAAoB,EAC7B,oBAAoB,WAChB,KAAK,CAAC,YAAY,IAAI,EAAE,YAGxB,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAsB,GAEnD,CACb,CAAC;AACN,CAAC,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"generate-token-dialog.d.ts","sourceRoot":"","sources":["../../../src/pages/user/generate-token-dialog.tsx"],"names":[],"mappings":"AAAA;;;;;;;;kFAQkF;AAElF,OAAO,EAAe,iBAAiB,EAA2C,MAAM,OAAO,CAAC;AAShG,eAAO,MAAM,mBAAmB,EAAE,iBAAiB,CAAC,wBAAwB,CAqI3E,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACrC,oBAAoB,EAAE,MAAM,IAAI,CAAC;CACpC"}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
/********************************************************************************
|
|
3
|
-
* Copyright (c) 2019 TypeFox and others
|
|
4
|
-
*
|
|
5
|
-
* This program and the accompanying materials are made available under the
|
|
6
|
-
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
-
* http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
-
*
|
|
9
|
-
* SPDX-License-Identifier: EPL-2.0
|
|
10
|
-
********************************************************************************/
|
|
11
|
-
import { useContext, useEffect, useState, useRef } from 'react';
|
|
12
|
-
import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, Box, TextField, DialogActions, Typography } from '@mui/material';
|
|
13
|
-
import { ButtonWithProgress } from '../../components/button-with-progress';
|
|
14
|
-
import { CopyToClipboard } from '../../components/copy-to-clipboard';
|
|
15
|
-
import { isError } from '../../extension-registry-types';
|
|
16
|
-
import { MainContext } from '../../context';
|
|
17
|
-
const TOKEN_DESCRIPTION_SIZE = 255;
|
|
18
|
-
export const GenerateTokenDialog = props => {
|
|
19
|
-
const [open, setOpen] = useState(false);
|
|
20
|
-
const [posted, setPosted] = useState(false);
|
|
21
|
-
const [description, setDescription] = useState('');
|
|
22
|
-
const [descriptionError, setDescriptionError] = useState();
|
|
23
|
-
const [token, setToken] = useState();
|
|
24
|
-
const context = useContext(MainContext);
|
|
25
|
-
const abortController = useRef(new AbortController());
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
document.addEventListener('keydown', handleEnter);
|
|
28
|
-
return () => {
|
|
29
|
-
abortController.current.abort();
|
|
30
|
-
document.removeEventListener('keydown', handleEnter);
|
|
31
|
-
};
|
|
32
|
-
}, []);
|
|
33
|
-
const handleOpenDialog = () => {
|
|
34
|
-
setOpen(true);
|
|
35
|
-
setPosted(false);
|
|
36
|
-
setDescription('');
|
|
37
|
-
setToken(undefined);
|
|
38
|
-
};
|
|
39
|
-
const handleClose = () => setOpen(false);
|
|
40
|
-
const handleDescriptionChange = (event) => {
|
|
41
|
-
const description = event.target.value;
|
|
42
|
-
let descriptionError;
|
|
43
|
-
if (description.length > TOKEN_DESCRIPTION_SIZE) {
|
|
44
|
-
descriptionError = `The description must not be longer than ${TOKEN_DESCRIPTION_SIZE} characters.`;
|
|
45
|
-
}
|
|
46
|
-
setDescription(description);
|
|
47
|
-
setDescriptionError(descriptionError);
|
|
48
|
-
};
|
|
49
|
-
const handleGenerate = async () => {
|
|
50
|
-
if (!context.user) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
setPosted(true);
|
|
54
|
-
try {
|
|
55
|
-
const token = await context.service.createAccessToken(abortController.current, context.user, description);
|
|
56
|
-
if (isError(token)) {
|
|
57
|
-
throw token;
|
|
58
|
-
}
|
|
59
|
-
setToken(token);
|
|
60
|
-
props.handleTokenGenerated();
|
|
61
|
-
}
|
|
62
|
-
catch (err) {
|
|
63
|
-
context.handleError(err);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
const handleEnter = (e) => {
|
|
67
|
-
if (e.code === 'Enter' && open && !token) {
|
|
68
|
-
handleGenerate();
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
return _jsxs(_Fragment, { children: [_jsx(Button, { variant: 'outlined', onClick: handleOpenDialog, children: "Generate new token" }), _jsxs(Dialog, { open: open, onClose: handleClose, children: [_jsx(DialogTitle, { children: "Generate new token" }), _jsxs(DialogContent, { children: [_jsx(DialogContentText, { children: "Describe where you will use this token." }), _jsx(Box, { my: 2, children: _jsx(TextField, { disabled: Boolean(token), fullWidth: true, label: 'Token Description', error: Boolean(descriptionError), helperText: descriptionError, onChange: handleDescriptionChange }) }), _jsx(TextField, { disabled: !token, margin: 'dense', label: 'Generated Token...', fullWidth: true, multiline: true, variant: 'outlined', rows: 4, value: token ? token.value : '' }), token ?
|
|
72
|
-
_jsx(Box, { children: _jsx(Typography, { sx: { color: 'red', fontWeight: 'bold' }, children: "Copy and paste this token to a safe place. It will not be displayed again." }) }) : null] }), _jsxs(DialogActions, { children: [token ?
|
|
73
|
-
_jsx(CopyToClipboard, { tooltipProps: { placement: 'left' }, children: ({ copy }) => (_jsx(Button, { autoFocus: true, variant: 'contained', color: 'secondary', onClick: () => {
|
|
74
|
-
copy(token.value);
|
|
75
|
-
setTimeout(handleClose, 700);
|
|
76
|
-
}, children: "Copy" })) }) : null, _jsx(Button, { onClick: handleClose, color: 'secondary', children: token ? 'Close' : 'Cancel' }), !token ?
|
|
77
|
-
_jsx(ButtonWithProgress, { autoFocus: true, sx: { ml: 1 }, error: Boolean(descriptionError), working: posted, onClick: handleGenerate, children: "Generate Token" }) : null] })] })] });
|
|
78
|
-
};
|
|
79
|
-
//# sourceMappingURL=generate-token-dialog.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"generate-token-dialog.js","sourceRoot":"","sources":["../../../src/pages/user/generate-token-dialog.tsx"],"names":[],"mappings":"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"}
|