-
Notifications
You must be signed in to change notification settings - Fork 549
Add ssh public keys on user-profile page #5223
Changes from 3 commits
fc8db98
c64fb10
963853e
c521801
29f3a22
aa22d17
84b9ff2
35ce9f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,16 +5,15 @@ import { cloneDeep, isEmpty, isNil } from 'lodash'; | |
import { TooltipIcon } from '../controls/tooltip-icon'; | ||
import { | ||
PAI_PLUGIN, | ||
USERSSH_TYPE_OPTIONS, | ||
SSH_KEY_BITS, | ||
PROTOCOL_TOOLTIPS, | ||
} from '../../utils/constants'; | ||
import { Hint } from '../sidebar/hint'; | ||
import { SSHPlugin } from '../../models/plugin/ssh-plugin'; | ||
import SSHGenerator from './ssh-generator'; | ||
|
||
import { | ||
DefaultButton, | ||
Dropdown, | ||
FontWeights, | ||
Toggle, | ||
Stack, | ||
|
@@ -63,16 +62,6 @@ export const JobSSH = ({ extras, onExtrasChange }) => { | |
[extras], | ||
); | ||
|
||
const _onUsersshTypeChange = useCallback( | ||
(_, item) => { | ||
_onChangeExtras('userssh', { | ||
type: item.key, | ||
value: '', | ||
}); | ||
}, | ||
[extras, _onChangeExtras], | ||
); | ||
|
||
const _onUsersshValueChange = useCallback( | ||
e => { | ||
_onChangeExtras('userssh', { | ||
|
@@ -128,36 +117,32 @@ export const JobSSH = ({ extras, onExtrasChange }) => { | |
onChange={_onUsersshEnable} | ||
/> | ||
{!isEmpty(sshPlugin.userssh) && ( | ||
<Stack horizontal gap='l1'> | ||
<Dropdown | ||
placeholder='Select user ssh key type...' | ||
options={USERSSH_TYPE_OPTIONS} | ||
onChange={_onUsersshTypeChange} | ||
selectedKey={sshPlugin.userssh.type} | ||
disabled={Object.keys(USERSSH_TYPE_OPTIONS).length <= 1} | ||
/> | ||
<TextField | ||
placeholder='Enter ssh public key' | ||
disabled={sshPlugin.userssh.type === 'none'} | ||
errorMessage={ | ||
isEmpty(sshPlugin.getUserSshValue()) | ||
? 'Please Enter Valid SSH public key' | ||
: null | ||
} | ||
onChange={_onUsersshValueChange} | ||
value={sshPlugin.getUserSshValue()} | ||
/> | ||
<DefaultButton onClick={ev => openSshGenerator(ev)}> | ||
SSH Key Generator | ||
</DefaultButton> | ||
{sshGenerator.isOpen && ( | ||
<SSHGenerator | ||
isOpen={sshGenerator.isOpen} | ||
bits={sshGenerator.bits} | ||
hide={hideSshGenerator} | ||
onSshKeysChange={_onSshKeysGenerated} | ||
<Stack gap='l1'> | ||
<Hint> | ||
Your pre-defined SSH public keys on the{' '} | ||
<a href='/user-profile.html'>User Profile</a> page will be set | ||
automatically. | ||
</Hint> | ||
<Stack horizontal gap='l1'> | ||
<TextField | ||
Lable='Add additional ssh public key' | ||
placeholder='Additional ssh public key' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
disabled={sshPlugin.userssh.type === 'none'} | ||
onChange={_onUsersshValueChange} | ||
value={sshPlugin.getUserSshValue()} | ||
/> | ||
)} | ||
<DefaultButton onClick={ev => openSshGenerator(ev)}> | ||
Generator | ||
</DefaultButton> | ||
{sshGenerator.isOpen && ( | ||
<SSHGenerator | ||
isOpen={sshGenerator.isOpen} | ||
bits={sshGenerator.bits} | ||
hide={hideSshGenerator} | ||
onSshKeysChange={_onSshKeysGenerated} | ||
/> | ||
)} | ||
</Stack> | ||
</Stack> | ||
)} | ||
</Stack> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -179,6 +179,26 @@ export const getUserRequest = async username => { | |
}); | ||
}; | ||
|
||
export const updateUserRequest = async (username, sskMessage) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated |
||
const url = `${config.restServerUri}/api/v2/users/me`; | ||
const token = checkToken(); | ||
return fetchWrapper(url, { | ||
method: 'PUT', | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
body: JSON.stringify({ | ||
data: { | ||
username: username, | ||
extension: { | ||
sshKeys: sskMessage, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated |
||
}, | ||
}, | ||
patch: true, | ||
}), | ||
}); | ||
}; | ||
|
||
export const getTokenRequest = async () => { | ||
return wrapper(() => client.token.getTokens()); | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,14 +24,17 @@ import { | |
listStorageDetailRequest, | ||
getGroupsRequest, | ||
updateBoundedClustersRequest, | ||
updateUserRequest, | ||
} from './conn'; | ||
|
||
import t from '../../components/tachyons.scss'; | ||
import { VirtualClusterDetailsList } from '../../home/home/virtual-cluster-statistics'; | ||
import TokenList from './user-profile/token-list'; | ||
import SSHlist from './user-profile/ssh-list'; | ||
import UserProfileHeader from './user-profile/header'; | ||
import StorageList from './user-profile/storage-list'; | ||
import BoundedClusterDialog from './user-profile/bounded-cluster-dialog'; | ||
import SSHListDialog from './user-profile/ssh-list-dialog'; | ||
import BoundedClusterList from './user-profile/bounded-cluster-list'; | ||
|
||
const UserProfileCard = ({ title, children, headerButton }) => { | ||
|
@@ -70,7 +73,11 @@ const UserProfile = () => { | |
const [showBoundedClusterDialog, setShowBoundedClusterDialog] = useState( | ||
false, | ||
); | ||
const [showAddSSHpublicKeysDialog, setShowAddSSHpublicKeysDialog] = useState( | ||
false, | ||
); | ||
const [processing, setProcessing] = useState(false); | ||
const [sshProcessing, setSSHProcessing] = useState(false); | ||
|
||
useEffect(() => { | ||
const fetchData = async () => { | ||
|
@@ -139,6 +146,39 @@ const UserProfile = () => { | |
}); | ||
}); | ||
|
||
// click `add public ssh keys button` -> open dialog | ||
const onAddPublicKeys = useCallback(async sshPublicKeys => { | ||
setSSHProcessing(true); | ||
let updatedSSHPublickeys = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please validate here: the user cannot add ssh keys with duplicate titles. (Title should be unique for one user) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
if (userInfo.extension.sshKeys) { | ||
updatedSSHPublickeys = cloneDeep(userInfo.extension.sshKeys); | ||
} | ||
updatedSSHPublickeys.push({ | ||
title: sshPublicKeys.title, | ||
value: sshPublicKeys.value, | ||
time: sshPublicKeys.time, | ||
}); | ||
await updateUserRequest(userInfo.username, updatedSSHPublickeys); | ||
const updatedUserInfo = await getUserRequest(userInfo.username); | ||
setUserInfo(updatedUserInfo); | ||
setSSHProcessing(false); | ||
}); | ||
|
||
const onDeleteSSHkeys = useCallback(async sshPublicKeys => { | ||
let updatedSSHPublickeys = []; | ||
if (userInfo.extension.sshKeys) { | ||
updatedSSHPublickeys = cloneDeep(userInfo.extension.sshKeys); | ||
} | ||
updatedSSHPublickeys = updatedSSHPublickeys.filter( | ||
item => | ||
item.title !== sshPublicKeys.title && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please only filter with Title should be unique and delete means delete an ssh key with a certain title. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
item.value !== sshPublicKeys.value, | ||
); | ||
await updateUserRequest(userInfo.username, updatedSSHPublickeys); | ||
const updatedUserInfo = await getUserRequest(userInfo.username); | ||
setUserInfo(updatedUserInfo); | ||
}); | ||
|
||
const onRevokeToken = useCallback(async token => { | ||
await revokeTokenRequest(token); | ||
await getTokenRequest().then(res => setTokens(res.tokens)); | ||
|
@@ -210,6 +250,29 @@ const UserProfile = () => { | |
> | ||
<TokenList tokens={tokens} onRevoke={onRevokeToken} /> | ||
</UserProfileCard> | ||
<UserProfileCard | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move this SSH is supposed to be used more frequently than tokens. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
title='SSH Public Keys' | ||
headerButton={ | ||
<DefaultButton | ||
onClick={() => setShowAddSSHpublicKeysDialog(true)} | ||
disabled={sshProcessing} | ||
> | ||
Add SSH Public Keys | ||
</DefaultButton> | ||
} | ||
> | ||
<SSHlist | ||
sshKeys={userInfo.extension.sshKeys} | ||
onDeleteSSHkeys={onDeleteSSHkeys} | ||
/> | ||
{/* dialog for add public ssh keys */} | ||
{showAddSSHpublicKeysDialog && ( | ||
<SSHListDialog | ||
onDismiss={() => setShowAddSSHpublicKeysDialog(false)} | ||
onAddPublickeys={onAddPublicKeys} | ||
/> | ||
)} | ||
</UserProfileCard> | ||
<UserProfileCard title='Storage'> | ||
<StorageList storageDetails={storageDetails} /> | ||
</UserProfileCard> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
import React, { useState } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { isEmpty } from 'lodash'; | ||
import { | ||
DefaultButton, | ||
PrimaryButton, | ||
DialogType, | ||
Dialog, | ||
DialogFooter, | ||
TextField, | ||
} from 'office-ui-fabric-react'; | ||
|
||
import t from '../../../components/tachyons.scss'; | ||
|
||
const SSHListDialog = ({ onDismiss, onAddPublickeys }) => { | ||
const [error, setError] = useState(''); | ||
const [inputTitleError, setInputTitleError] = useState(''); | ||
const [inputValueError, setInputValueError] = useState(''); | ||
const [processing, setProcessing] = useState(false); | ||
const [title, setTitle] = useState(''); | ||
const [value, setValue] = useState(''); | ||
|
||
const onAddAsync = async () => { | ||
if (title.trim() === '') { | ||
setInputTitleError('Please input title'); | ||
} else if (value.trim() === '') { | ||
setInputValueError('Please input ssk value'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please validate the You can use this regex: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
setProcessing(true); | ||
try { | ||
await onAddPublickeys({ | ||
title: title.trim(), | ||
value: value.trim(), | ||
time: new Date().getTime(), | ||
}); | ||
} catch (error) { | ||
setError(error.message); | ||
} finally { | ||
setProcessing(false); | ||
onDismiss(); | ||
} | ||
} | ||
}; | ||
|
||
return ( | ||
<div> | ||
<Dialog | ||
hidden={false} | ||
onDismiss={onDismiss} | ||
dialogContentProps={{ | ||
type: DialogType.normal, | ||
title: 'Add SSH Public Keys', | ||
}} | ||
modalProps={{ | ||
isBlocking: true, | ||
}} | ||
minWidth={600} | ||
> | ||
<div> | ||
<div className={t.mt1}> | ||
<TextField | ||
label='title' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to give the user some hints.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
required={true} | ||
errorMessage={inputTitleError} | ||
onChange={e => { | ||
setTitle(e.target.value); | ||
setInputTitleError(null); | ||
}} | ||
validateOnFocusOut={true} | ||
/> | ||
</div> | ||
<div className={t.mt1}> | ||
<TextField | ||
label='value' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to give the user some hints.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
required={true} | ||
errorMessage={inputValueError} | ||
onChange={e => { | ||
setValue(e.target.value); | ||
setInputValueError(null); | ||
}} | ||
multiline | ||
rows={5} | ||
validateOnFocusOut={true} | ||
/> | ||
</div> | ||
</div> | ||
<DialogFooter> | ||
<PrimaryButton | ||
onClick={onAddAsync} | ||
disabled={processing} | ||
text='Add' | ||
/> | ||
<DefaultButton | ||
onClick={onDismiss} | ||
disabled={processing} | ||
text='Cancel' | ||
/> | ||
</DialogFooter> | ||
</Dialog> | ||
<Dialog | ||
hidden={isEmpty(error)} | ||
onDismiss={() => setError('')} | ||
dialogContentProps={{ | ||
type: DialogType.normal, | ||
title: 'Error', | ||
subText: error, | ||
}} | ||
modalProps={{ | ||
isBlocking: true, | ||
}} | ||
> | ||
<DialogFooter> | ||
<DefaultButton onClick={() => setError('')}>OK</DefaultButton> | ||
</DialogFooter> | ||
</Dialog> | ||
</div> | ||
); | ||
}; | ||
|
||
SSHListDialog.propTypes = { | ||
onDismiss: PropTypes.func.isRequired, | ||
onAddPublickeys: PropTypes.func.isRequired, | ||
}; | ||
|
||
export default SSHListDialog; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please change
ssh
toSSH
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok