Skip to content

Commit 96116db

Browse files
committed
feat: display elevation in the clients page
1 parent 624ab65 commit 96116db

5 files changed

Lines changed: 130 additions & 3 deletions

File tree

ui/src/client/ClientStore.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,11 @@ export class ClientStore extends BaseStore<IClient> {
3838
await this.createNoNotifcation(name);
3939
this.snack('Client added');
4040
};
41+
42+
@action
43+
public elevate = async (id: number, durationSeconds: number): Promise<void> => {
44+
await axios.post(`${config.get('url')}client:elevate`, {id, durationSeconds});
45+
await this.refresh();
46+
this.snack('Client elevated');
47+
};
4148
}

ui/src/client/Clients.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,27 @@ import TableHead from '@mui/material/TableHead';
99
import TableRow from '@mui/material/TableRow';
1010
import Delete from '@mui/icons-material/Delete';
1111
import Edit from '@mui/icons-material/Edit';
12+
import Security from '@mui/icons-material/Security';
1213
import Button from '@mui/material/Button';
14+
import Tooltip from '@mui/material/Tooltip';
15+
import TimeAgo from 'react-timeago';
1316
import ConfirmDialog from '../common/ConfirmDialog';
1417
import DefaultPage from '../common/DefaultPage';
1518
import AddClientDialog from './AddClientDialog';
1619
import UpdateClientDialog from './UpdateClientDialog';
20+
import ElevateClientDialog from './ElevateClientDialog';
1721
import {IClient} from '../types';
1822
import CopyableSecret from '../common/CopyableSecret';
1923
import {LastUsedCell} from '../common/LastUsedCell';
24+
import {TimeAgoFormatter} from '../common/TimeAgoFormatter';
2025
import {observer} from 'mobx-react-lite';
2126
import {useStores} from '../stores';
2227

2328
const Clients = observer(() => {
2429
const {clientStore} = useStores();
2530
const [toDeleteClient, setToDeleteClient] = useState<IClient>();
2631
const [toUpdateClient, setToUpdateClient] = useState<IClient>();
32+
const [toElevateClient, setToElevateClient] = useState<IClient>();
2733
const [createDialog, setCreateDialog] = useState<boolean>(false);
2834
const clients = clientStore.getItems();
2935

@@ -32,6 +38,7 @@ const Clients = observer(() => {
3238
return (
3339
<DefaultPage
3440
title="Clients"
41+
maxWidth={1000}
3542
rightControl={
3643
<Button
3744
id="create-client"
@@ -49,6 +56,8 @@ const Clients = observer(() => {
4956
<TableCell>Name</TableCell>
5057
<TableCell style={{width: 200}}>Token</TableCell>
5158
<TableCell>Last Used</TableCell>
59+
<TableCell>Elevation ends</TableCell>
60+
<TableCell />
5261
<TableCell />
5362
<TableCell />
5463
</TableRow>
@@ -60,8 +69,10 @@ const Clients = observer(() => {
6069
name={client.name}
6170
value={client.token}
6271
lastUsed={client.lastUsed}
72+
elevatedUntil={client.elevatedUntil}
6373
fEdit={() => setToUpdateClient(client)}
6474
fDelete={() => setToDeleteClient(client)}
75+
fElevate={() => setToElevateClient(client)}
6576
/>
6677
))}
6778
</TableBody>
@@ -90,6 +101,13 @@ const Clients = observer(() => {
90101
requireElevated
91102
/>
92103
)}
104+
{toElevateClient != null && (
105+
<ElevateClientDialog
106+
clientName={toElevateClient.name}
107+
clientId={toElevateClient.id}
108+
fClose={() => setToElevateClient(undefined)}
109+
/>
110+
)}
93111
</DefaultPage>
94112
);
95113
});
@@ -98,11 +116,13 @@ interface IRowProps {
98116
name: string;
99117
value: string;
100118
lastUsed: string | null;
119+
elevatedUntil?: string;
101120
fEdit: VoidFunction;
102121
fDelete: VoidFunction;
122+
fElevate: VoidFunction;
103123
}
104124

105-
const Row = ({name, value, lastUsed, fEdit, fDelete}: IRowProps) => (
125+
const Row = ({name, value, lastUsed, elevatedUntil, fEdit, fDelete, fElevate}: IRowProps) => (
106126
<TableRow>
107127
<TableCell>{name}</TableCell>
108128
<TableCell>
@@ -114,6 +134,20 @@ const Row = ({name, value, lastUsed, fEdit, fDelete}: IRowProps) => (
114134
<TableCell>
115135
<LastUsedCell lastUsed={lastUsed} />
116136
</TableCell>
137+
<TableCell>
138+
{elevatedUntil && Date.parse(elevatedUntil) > Date.now() ? (
139+
<TimeAgo date={elevatedUntil} formatter={TimeAgoFormatter.long} />
140+
) : (
141+
'-'
142+
)}
143+
</TableCell>
144+
<TableCell align="right" padding="none">
145+
<Tooltip title="Elevate">
146+
<IconButton onClick={fElevate} className="elevate">
147+
<Security />
148+
</IconButton>
149+
</Tooltip>
150+
</TableCell>
117151
<TableCell align="right" padding="none">
118152
<IconButton onClick={fEdit} className="edit">
119153
<Edit />
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, {useState} from 'react';
2+
import Button from '@mui/material/Button';
3+
import Dialog from '@mui/material/Dialog';
4+
import DialogActions from '@mui/material/DialogActions';
5+
import DialogContent from '@mui/material/DialogContent';
6+
import DialogTitle from '@mui/material/DialogTitle';
7+
import MenuItem from '@mui/material/MenuItem';
8+
import Select from '@mui/material/Select';
9+
import FormControl from '@mui/material/FormControl';
10+
import InputLabel from '@mui/material/InputLabel';
11+
import {observer} from 'mobx-react-lite';
12+
import {useStores} from '../stores';
13+
import ElevationForm from '../common/ElevationForm';
14+
15+
interface IProps {
16+
clientName: string;
17+
clientId: number;
18+
fClose: VoidFunction;
19+
}
20+
21+
const durationOptions = [
22+
{label: 'Cancel elevation', seconds: -1},
23+
{label: '1 hour', seconds: 60 * 60},
24+
{label: '1 day', seconds: 24 * 60 * 60},
25+
{label: '30 days', seconds: 30 * 24 * 60 * 60},
26+
{label: '1 year', seconds: 365 * 24 * 60 * 60},
27+
];
28+
29+
const ElevateClientDialog = observer(({clientName, clientId, fClose}: IProps) => {
30+
const {elevateStore, clientStore, currentUser} = useStores();
31+
const [durationSeconds, setDurationSeconds] = useState(durationOptions[1].seconds);
32+
33+
const needsElevation = !elevateStore.elevated;
34+
35+
const handleConfirm = async () => {
36+
await clientStore.elevate(clientId, durationSeconds);
37+
if (clientId === currentUser.user.clientId) {
38+
currentUser.tryAuthenticate();
39+
}
40+
fClose();
41+
};
42+
43+
const handleClose = () => {
44+
elevateStore.cleanupOidcElevate();
45+
fClose();
46+
};
47+
48+
return (
49+
<Dialog open={true} onClose={handleClose} aria-labelledby="elevate-client-dialog-title">
50+
<DialogTitle id="elevate-client-dialog-title">Elevate Client: {clientName}</DialogTitle>
51+
<DialogContent>
52+
{needsElevation ? (
53+
<ElevationForm />
54+
) : (
55+
<FormControl fullWidth style={{marginTop: 8}}>
56+
<InputLabel id="elevate-duration-label">Duration</InputLabel>
57+
<Select
58+
labelId="elevate-duration-label"
59+
label="Duration"
60+
value={durationSeconds}
61+
onChange={(e) => setDurationSeconds(e.target.value as number)}>
62+
{durationOptions.map((opt) => (
63+
<MenuItem key={opt.seconds} value={opt.seconds}>
64+
{opt.label}
65+
</MenuItem>
66+
))}
67+
</Select>
68+
</FormControl>
69+
)}
70+
</DialogContent>
71+
<DialogActions>
72+
<Button onClick={handleClose}>Cancel</Button>
73+
{!needsElevation && (
74+
<Button onClick={handleConfirm} autoFocus color="primary" variant="contained">
75+
Elevate
76+
</Button>
77+
)}
78+
</DialogActions>
79+
</Dialog>
80+
);
81+
});
82+
83+
export default ElevateClientDialog;

ui/src/tests/client.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ enum Col {
1919
Name = 1,
2020
Token = 2,
2121
LastSeen = 3,
22-
Edit = 4,
23-
Delete = 5,
22+
ElevationEnds = 4,
23+
Elevate = 5,
24+
Edit = 6,
25+
Delete = 7,
2426
}
2527

2628
const waitForClient =

ui/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface IClient {
1515
token: string;
1616
name: string;
1717
lastUsed: string | null;
18+
elevatedUntil?: string;
1819
}
1920

2021
export interface IPlugin {

0 commit comments

Comments
 (0)