feat(new VM): add ssh key to vm creation form#9413
Conversation
|
Linked to Plane Work Item(s) References This comment was auto-generated by Plane |
f0d8f6b to
04bfe1b
Compare
| const addSshKey = () => { | ||
| if (!vmState.ssh_key.trim()) { | ||
| isSshKeyEmpty.value = true | ||
| return | ||
| } | ||
| vmState.sshKeys.push(vmState.ssh_key.trim()) | ||
| vmState.ssh_key = '' | ||
| isSshKeyEmpty.value = false | ||
| } | ||
|
|
||
| const removeSshKey = (index: number) => { | ||
| vmState.sshKeys.splice(index, 1) | ||
| } | ||
|
|
There was a problem hiding this comment.
For consistency with XO5, additional validation like duplication detection is needed. And maybe check if it starts with ssh-rsa for validating the key.
| const addSshKey = () => { | |
| if (!vmState.ssh_key.trim()) { | |
| isSshKeyEmpty.value = true | |
| return | |
| } | |
| vmState.sshKeys.push(vmState.ssh_key.trim()) | |
| vmState.ssh_key = '' | |
| isSshKeyEmpty.value = false | |
| } | |
| const removeSshKey = (index: number) => { | |
| vmState.sshKeys.splice(index, 1) | |
| } | |
| const addSshKey = () => { | |
| const sshKey = vmState.ssh_key.trim() | |
| if (!sshKey) { | |
| isSshKeyError.value = 'empty' | |
| return | |
| } | |
| if (vmState.sshKeys.includes(sshKey)) { | |
| isSshKeyError.value = 'alreadyExist' | |
| return | |
| } | |
| if (!sshKey.startsWith('ssh-rsa ')) { | |
| isSshKeyError.value = 'invalidKey' | |
| return | |
| } | |
| vmState.sshKeys.push(sshKey) | |
| vmState.ssh_key = '' | |
| isSshKeyError.value = false | |
| } | |
There was a problem hiding this comment.
I agree on the duplication detection.
But, ssh-rsa!
There was a problem hiding this comment.
Perhaps you should check for all types of SSH keys, or just if start with ssh-?
The ssh-keygen can only generate ecdsa | ecdsa-sk | ed25519 | ed25519-sk | rsa types, which makes it small enough to test all of them.
| <div class="ssh-chips"> | ||
| <div v-for="(key, index) in vmState.sshKeys" :key="index" class="ssh-chip-wrapper"> | ||
| <UiChip accent="info" @remove="removeSshKey(index)"> | ||
| {{ key }} |
There was a problem hiding this comment.
For consistency with XO5, add an sshKey parser to extract the name of the key.
const parseSshKey = (key: string) => {
const parts = key.trim().split(' ')
if (parts.length >= 3) {
return parts[2]
}
return key
}There was a problem hiding this comment.
After discussion, we are keeping the Figma design for now
| <div class="ssh-key-area"> | ||
| <UiTextarea v-model="vmState.ssh_key" required :accent="isSshKeyEmpty ? 'danger' : 'brand'"> | ||
| {{ t('public-key') }} | ||
| <template v-if="isSshKeyEmpty" #info> | ||
| {{ t('public-key-mandatory') }} | ||
| </template> | ||
| </UiTextarea> | ||
| </div> |
There was a problem hiding this comment.
For greater clarity regarding error detection, additional error messages should be added.
| <div class="ssh-key-area"> | |
| <UiTextarea v-model="vmState.ssh_key" required :accent="isSshKeyEmpty ? 'danger' : 'brand'"> | |
| {{ t('public-key') }} | |
| <template v-if="isSshKeyEmpty" #info> | |
| {{ t('public-key-mandatory') }} | |
| </template> | |
| </UiTextarea> | |
| </div> | |
| <div class="ssh-key-area"> | |
| <UiTextarea | |
| v-model="vmState.ssh_key" | |
| required | |
| :accent="isSshKeyError !== false ? 'danger' : 'brand'" | |
| > | |
| {{ t('public-key') }} | |
| <template v-if="isSshKeyError == 'empty'" #info> | |
| {{ t('public-key-mandatory') }} | |
| </template> | |
| <template v-else-if="isSshKeyError == 'alreadyExist'" #info> | |
| {{ t('public-key-already-exist') }} | |
| </template> | |
| <template v-else-if="isSshKeyError == 'invalidKey'" #info> | |
| {{ t('public-key-invalid') }} | |
| </template> | |
| </UiTextarea> | |
| </div> |
There was a problem hiding this comment.
isSshKeyError == 'empty'
| // Toaster | ||
| const errorMessage = ref('') | ||
| const isToasterOpen = ref(false) | ||
| const isSshKeyEmpty = ref(false) |
There was a problem hiding this comment.
| const isSshKeyEmpty = ref(false) | |
| const isSshKeyError = ref<'empty' | 'alreadyExist' | 'invalidKey' | false>(false) |
There was a problem hiding this comment.
I'm not a fan of having mixed types for the same variable (boolean and string here).
Plus, the naming is confusing.
Maybe a better approach would be to have two variables, one boolean for the "state" and another for the "value".
| const { properties } = useXoTaskPropertiesUtils(() => task) | ||
|
|
||
| const cloudConfig = computed(() => { | ||
| const args = properties.value.other?.args as { cloud_config?: string } | undefined |
There was a problem hiding this comment.
I think you can change this type on xo.d.ts
| <VtsRecursiveFields :fields="properties.other" /> | ||
| <VtsRecursiveFields :fields="propertiesOtherWithoutCloudConfig" /> | ||
| </div> | ||
| <UiLogEntryViewer :content="cloudConfig" :label="t('cloud-config')" size="small" accent="info" /> |
There was a problem hiding this comment.
The other fields on this card is not translated, so maybe you don't have to translate this too. Or translate all fields.
to discuss
| <UiLogEntryViewer :content="cloudConfig" :label="t('cloud-config')" size="small" accent="info" /> | |
| <UiLogEntryViewer :content="cloudConfig" :label="cloud_config" size="small" accent="info" /> |
There was a problem hiding this comment.
Since we know we want to display this property explicitly, it makes sense to have a translation. However, I'm not sure of the underscore in the translation value (Could_config -> Could config).
OlivierFL
left a comment
There was a problem hiding this comment.
Remember to add an entry in the changelog.
| .install-ssh-key-container { | ||
| margin-block-start: 3rem; | ||
| } |
There was a problem hiding this comment.
I don't think this space is needed: while the design in Figma is newer, there is only 2.4rem of gap, which what we already have in .install-settings-container.
Plus, you can remove the unnecessary <div> in the template.
| .ssh-chips { | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: 0.5rem; |
There was a problem hiding this comment.
| gap: 0.5rem; | |
| gap: 0.4rem; |
| } | ||
| } | ||
| ) | ||
| watch(() => [vmState.installMode, vmState.name, vmState.sshKeys], buildCloudConfig) |
There was a problem hiding this comment.
As discussed, I think you're watching too many properties, watching only vmState.sshKeys should be enough.
Here the problem is that buildCloudConfig is triggered too many times, even when it shouldn't, and from my tests, sometimes it is not triggered when it should.
vmState.installModeis not useful here, since you already check its value inbuildCloudConfig- same for
vmState.nameI think
| const addSshKey = () => { | ||
| if (!vmState.ssh_key.trim()) { | ||
| isSshKeyEmpty.value = true | ||
| return | ||
| } | ||
| vmState.sshKeys.push(vmState.ssh_key.trim()) | ||
| vmState.ssh_key = '' | ||
| isSshKeyEmpty.value = false | ||
| } | ||
|
|
||
| const removeSshKey = (index: number) => { | ||
| vmState.sshKeys.splice(index, 1) | ||
| } | ||
|
|
There was a problem hiding this comment.
I agree on the duplication detection.
But, ssh-rsa!
| method: vmState.installMode, | ||
| repository: vmState.installMode === 'network' ? '' : vmState.selectedVdi, | ||
| vmState.installMode !== 'no-config' && | ||
| vmState.installMode !== 'ssh-key' && { |
There was a problem hiding this comment.
It may be not a change to do on this PR, but it will be nice to extract the installMode values in a constant instead of hardcoding them in multiple places.
| .ssh-chips { | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: 0.5rem; | ||
| margin-block-end: 1rem; | ||
| width: 100%; | ||
|
|
||
| .ssh-chip-wrapper { | ||
| min-width: 0; | ||
| max-width: 40rem; | ||
| display: flex; | ||
| } | ||
| } |
There was a problem hiding this comment.
Not to be done on this PR, but I see in Figma that we should have a UiChipsList component, that would act as the wrapper here.
Maybe add a TODO to update this part when we have the component ready.
@UnelDev it can be done on your XO-1890 card.
| <div class="ssh-key-area"> | ||
| <UiTextarea v-model="vmState.ssh_key" required :accent="isSshKeyEmpty ? 'danger' : 'brand'"> | ||
| {{ t('public-key') }} | ||
| <template v-if="isSshKeyEmpty" #info> | ||
| {{ t('public-key-mandatory') }} | ||
| </template> | ||
| </UiTextarea> | ||
| </div> |
There was a problem hiding this comment.
isSshKeyError == 'empty'
| const propertiesOtherWithoutCloudConfig = computed(() => { | ||
| const other = properties.value.other | ||
| const args = other?.args as { cloud_config?: string } | undefined | ||
|
|
||
| if (!args?.cloud_config) { | ||
| return other | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const { cloud_config, ...argsCopy } = args | ||
|
|
||
| return { | ||
| ...other, | ||
| args: argsCopy, | ||
| } | ||
| }) |
There was a problem hiding this comment.
What about something like this, using omit from Lodash:
| const propertiesOtherWithoutCloudConfig = computed(() => { | |
| const other = properties.value.other | |
| const args = other?.args as { cloud_config?: string } | undefined | |
| if (!args?.cloud_config) { | |
| return other | |
| } | |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |
| const { cloud_config, ...argsCopy } = args | |
| return { | |
| ...other, | |
| args: argsCopy, | |
| } | |
| }) | |
| const propertiesOtherWithoutCloudConfig = computed(() => properties.value.other?.args ? omit(properties.value.other.args, 'cloud_config') : undefined) |
| <VtsRecursiveFields :fields="properties.other" /> | ||
| <VtsRecursiveFields :fields="propertiesOtherWithoutCloudConfig" /> | ||
| </div> | ||
| <UiLogEntryViewer :content="cloudConfig" :label="t('cloud-config')" size="small" accent="info" /> |
There was a problem hiding this comment.
Careful here, is cloudConfig is undefined, the component will be displayed as empty.
| <UiLogEntryViewer :content="cloudConfig" :label="t('cloud-config')" size="small" accent="info" /> | |
| <UiLogEntryViewer v-if="cloudConfig !== undefined" :content="cloudConfig" :label="t('cloud-config')" size="small" accent="info" /> |
| <VtsRecursiveFields :fields="properties.other" /> | ||
| <VtsRecursiveFields :fields="propertiesOtherWithoutCloudConfig" /> | ||
| </div> | ||
| <UiLogEntryViewer :content="cloudConfig" :label="t('cloud-config')" size="small" accent="info" /> |
There was a problem hiding this comment.
Since we know we want to display this property explicitly, it makes sense to have a translation. However, I'm not sure of the underscore in the translation value (Could_config -> Could config).
Description
Add ssh key field to VM creation form and updating the
Cloud_configdisplay on the site/task side panel🔗 XO-1521
A quick note:
Regarding the
TaskPropertiesCard.vuefile, I'm not entirely happy with disabling ESLint for my code.I found another solution (below), but I'm not convinced.
Screenshots :
Create VM view:
Site/task sidepanel view:
Checklist
Fixes #007,See xoa-support#42,See https://...)Introduced byCHANGELOG.unreleased.mdReview process
If you are an external contributor, you can skip this part. Simply create the pull request, and we'll get back to you as soon as possible.
Notes: