Add pkg/harvester components + shell portings - 1

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia
2024-10-23 17:00:46 +02:00
parent f8408469f7
commit 4f2688f6ab
265 changed files with 42316 additions and 82 deletions
@@ -0,0 +1,191 @@
<script>
import { mapGetters } from 'vuex';
import { clone } from '@shell/utils/object';
import { randomStr } from '@shell/utils/string';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ModalWithCard from '@shell/components/ModalWithCard';
const _NEW = '_NEW';
export default {
components: {
LabeledInput,
LabeledSelect,
ModalWithCard,
},
props: {
value: {
type: [Array, String],
default: () => {
return [];
}
},
resource: {
type: Object,
default: () => {
return {};
}
},
userOptions: {
type: Array,
default: () => {
return [];
}
},
multiple: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'create',
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
selectedUser: this.value,
newUsername: '',
randomStr: randomStr(5).toLowerCase(),
errors: [],
isOpen: false,
};
},
computed: { ...mapGetters({ t: 'i18n/t' }) },
watch: {
value(neu) {
this.selectedUser = neu;
},
selectedUser(val, old) {
if ( val.includes(_NEW)) {
this.$set(this, 'selectedUser', old);
this.update();
this.show();
}
}
},
methods: {
update() {
this.$emit('update:user', clone(this.selectedUser));
},
addUser(buttonCb) {
const reg = /^[-._0-9a-zA-Z]+$/;
this.errors = [];
if (!reg.test(this.newUsername)) {
const message = this.t('harvester.virtualMachine.accessCredentials.invalidUser');
this.errors.push(message);
buttonCb(false);
return;
}
if (this.userOptions.includes(this.newUsername)) {
const message = this.t('harvester.virtualMachine.accessCredentials.duplicatedUser');
this.errors.push(message);
buttonCb(false);
return;
}
if (this.multiple) {
this.selectedUser.push(this.newUsername);
} else {
this.selectedUser = this.newUsername;
}
this.$emit('update:newUser', this.newUsername);
this.update();
buttonCb(true);
this.cancel();
},
show() {
this.isOpen = true;
},
hide() {
this.isOpen = false;
},
cancel() {
this.hide();
this.newUsername = '';
},
}
};
</script>
<template>
<div class="vm__credentials-users">
<LabeledSelect
v-model="selectedUser"
:options="userOptions"
:label="t('harvester.virtualMachine.accessCredentials.injectSSH.users')"
:taggable="multiple"
:mode="mode"
:multiple="multiple"
:searchable="false"
:tooltip="t('harvester.virtualMachine.accessCredentials.userTips')"
:class="{'hasUsers': selectedUser.length > 0}"
@input="update"
/>
<ModalWithCard
v-if="isOpen"
:name="randomStr"
width="40%"
:errors="errors"
@finish="addUser"
@close="cancel"
>
<template #title>
{{ t('harvester.virtualMachine.accessCredentials.injectSSH.addUser') }}
</template>
<template #content>
<LabeledInput
v-model="newUsername"
:label="t('harvester.virtualMachine.input.username')"
class="mb-20"
required
@keydown.native.enter.prevent="()=>{}"
/>
</template>
</ModalWithCard>
</div>
</template>
<style lang="scss">
.vm__credentials-users {
.hasUsers.labeled-select.edit.taggable {
.labeled-tooltip {
.icon.status-icon {
top: 30px;
}
}
}
}
</style>
@@ -0,0 +1,212 @@
<script>
import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner';
import InfoBox from '@shell/components/InfoBox';
import { randomStr } from '@shell/utils/string';
import { removeObject } from '@shell/utils/array';
import { ACCESS_CREDENTIALS } from '../../../config/harvester-map';
import { _EDIT } from '@shell/config/query-params';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
const _NEW = '_NEW';
export default {
components: {
Banner,
InfoBox
},
props: {
value: {
type: Array,
default: () => {
return [];
}
},
resource: {
type: Object,
default: () => {
return {};
}
},
mode: {
type: String,
default: 'create',
},
disableCreate: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
isQemuInstalled: {
type: Boolean,
default: false
}
},
data() {
return {
ACCESS_CREDENTIALS,
toRemove: [],
toSave: [],
rows: [],
newUserOptions: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
isEdit() {
return this.mode === _EDIT;
},
historyUsersOptions() {
const out = JSON.parse(this.resource?.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS] || '[]');
out.unshift({
label: this.t('harvester.virtualMachine.accessCredentials.injectSSH.addUser'),
value: _NEW,
});
return out;
},
userOptions() {
return Array.from(new Set([...this.historyUsersOptions, ...this.newUserOptions]));
},
},
watch: {
value: {
handler(neu) {
this.rows = [...neu];
},
deep: true,
immediate: true,
},
},
methods: {
addCredentials(type) {
const neu = {
source: type,
users: [],
sshkeys: [],
username: '',
newPassword: '',
secretName: this.generateSecretName(this.resource.metadata.name),
};
this.rows.push(neu);
this.update();
},
remove(row) {
removeObject(this.rows, row);
this.update();
},
update() {
this.$emit('input', this.rows);
},
updateNewUser(newUser) {
this.newUserOptions.push(newUser);
},
generateSecretName(name) {
return name ? `${ name }-${ randomStr(5).toLowerCase() }` : undefined;
},
componentFor(type) {
switch (type) {
case ACCESS_CREDENTIALS.RESET_PWD:
return require(`./type/basicAuth.vue`).default;
case ACCESS_CREDENTIALS.INJECT_SSH:
return require(`./type/sshkey.vue`).default;
}
},
headerFor(type) {
return {
userPassword: this.$store.getters['i18n/t']('harvester.virtualMachine.accessCredentials.resetPwd.label'),
sshPublicKey: this.$store.getters['i18n/t']('harvester.virtualMachine.accessCredentials.injectSSH.label'),
}[type];
},
}
};
</script>
<template>
<div>
<Banner class="mb-20" color="info" :label="t('harvester.virtualMachine.accessCredentials.tips')" />
<div v-for="(row, i) in rows" :key="row.id">
<InfoBox class="volume-source">
<button v-if="isEdit" type="button" class="role-link btn btn-sm remove-vol" @click="remove(row)">
<i class="icon icon-x" />
</button>
<h3>
<span>
{{ headerFor(row.source) }}
</span>
</h3>
<div>
<component
:is="componentFor(row.source)"
v-model="rows[i]"
:rows="rows"
:resource="resource"
:user-options="userOptions"
:mode="mode"
:idx="i"
@update="update"
@update:newUser="updateNewUser"
/>
</div>
</InfoBox>
</div>
<div v-if="isEdit && isQemuInstalled" class="mt-20">
<button
type="button"
class="btn btn-sm bg-primary mr-15 mb-10"
@click="addCredentials(ACCESS_CREDENTIALS.RESET_PWD)"
>
{{ t('harvester.virtualMachine.accessCredentials.resetPwd.label') }}
</button>
<button
type="button"
class="btn btn-sm bg-primary mr-15 mb-10"
@click="addCredentials(ACCESS_CREDENTIALS.INJECT_SSH)"
>
{{ t('harvester.virtualMachine.accessCredentials.injectSSH.label') }}
</button>
</div>
</div>
</template>
<style lang='scss' scoped>
.volume-source {
position: relative;
}
.remove-vol {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
</style>
@@ -0,0 +1,94 @@
<script>
import Password from '@shell/components/form/Password';
import AccessCredentialsUsers from '../AccessCredentialsUsers';
export default {
name: 'HarvesterEditVolume',
components: { Password, AccessCredentialsUsers },
props: {
mode: {
type: String,
default: 'create'
},
resource: {
type: Object,
default: () => {
return {};
}
},
userOptions: {
type: Array,
default: () => {
return [];
}
},
value: {
type: Object,
default: () => {
return {};
}
},
},
data() {
return {
defaultPwdValue: {
username: '',
newPassword: ''
}
};
},
methods: {
update() {
this.$emit('update');
},
updateUser(neu) {
this.$set(this.value, 'username', neu);
this.update();
},
updateNewUser(neu) {
this.$emit('update:newUser', neu);
}
},
};
</script>
<template>
<div class="row" @input="update">
<div class="col span-6">
<AccessCredentialsUsers
v-model="value.username"
:resource="resource"
:user-options="userOptions"
:multiple="false"
:mode="mode"
@update:user="updateUser"
@update:newUser="updateNewUser"
/>
</div>
<div class="col span-6">
<Password
ref="password"
v-model="value.newPassword"
:mode="mode"
:disabled="mode !== 'edit'"
:label="t('harvester.virtualMachine.input.password')"
required
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.action {
display: flex;
flex-direction: row-reverse;
}
</style>
@@ -0,0 +1,85 @@
<script>
import SSHKey from '../../VirtualMachineSSHKey';
import AccessCredentialsUsers from '../AccessCredentialsUsers';
export default {
name: 'HarvesterEditVolume',
components: { SSHKey, AccessCredentialsUsers },
props: {
mode: {
type: String,
default: 'create'
},
resource: {
type: Object,
default: () => {
return {};
}
},
userOptions: {
type: Array,
default: () => {
return [];
}
},
value: {
type: Object,
default: () => {
return {};
}
},
},
methods: {
update() {
this.$emit('update');
},
updateUser(neu) {
this.$set(this.value, 'users', neu);
this.update();
},
updateSSH(neu) {
this.$set(this.value, 'sshkeys', neu);
this.update();
},
updateNewUser(neu) {
this.$emit('update:newUser', neu);
}
},
};
</script>
<template>
<div>
<div class="columns row">
<div class="col span-6">
<AccessCredentialsUsers
v-model="value.users"
:resource="resource"
:user-options="userOptions"
:mode="mode"
:multiple="true"
@update:user="updateUser"
@update:newUser="updateNewUser"
/>
</div>
<div class="col span-6">
<SSHKey
v-model="value.sshkeys"
class="mb-20"
:namespace="resource.metadata.namespace"
:mode="mode"
:searchable="false"
@update:sshKey="updateSSH"
/>
</div>
</div>
</div>
</template>