mirror of https://github.com/rancher/dashboard.git
HARVESTER: feat: setting & sshKey page
This commit is contained in:
parent
9c72e6c376
commit
87d104d27b
|
|
@ -4041,6 +4041,8 @@ tableHeaders:
|
|||
value: Value
|
||||
version: Version
|
||||
weight: Weight
|
||||
progress: Progress
|
||||
fingerprint: Fingerprint
|
||||
|
||||
target:
|
||||
router:
|
||||
|
|
@ -4656,6 +4658,42 @@ workload:
|
|||
tip: The duration the pod needs to terminate successfully.
|
||||
title: Upgrading
|
||||
|
||||
harvester:
|
||||
dashboard:
|
||||
label: Dashboard
|
||||
host:
|
||||
label: Hosts
|
||||
virtualMachine:
|
||||
label: Virtual Machines
|
||||
volume:
|
||||
label: Volumes
|
||||
image:
|
||||
label: Images
|
||||
vmTemplate:
|
||||
label: Templates
|
||||
backup:
|
||||
label: Backups
|
||||
network:
|
||||
label: Networks
|
||||
sshKey:
|
||||
label: SSH Keys
|
||||
keypair: SSH Key
|
||||
tabs:
|
||||
basics: Basics
|
||||
|
||||
setting:
|
||||
label: Settings
|
||||
validation:
|
||||
physicalNIC: DefaultPhysicalNIC
|
||||
placeholder:
|
||||
accessKeyId: specify your access key id
|
||||
secretAccessKey: specify your secret access key
|
||||
cert: upload a self-signed SSL certificate
|
||||
vlanChangeTip: The newly modified default physical NIC only applies to newly added nodes, not existing ones
|
||||
defaultPhysicalNIC: Default Physical NIC
|
||||
|
||||
cloudTemplate:
|
||||
label: Cloud Config Templates
|
||||
|
||||
##############################
|
||||
# Model Properties
|
||||
|
|
@ -4951,6 +4989,62 @@ typeLabel:
|
|||
one { Workload }
|
||||
other { Workloads }
|
||||
}
|
||||
kubevirt.io.virtualmachine: |-
|
||||
{count, plural,
|
||||
one { Virtual Machine }
|
||||
other { Virtual Machines }
|
||||
}
|
||||
harvesterhci.io.virtualmachineimage: |-
|
||||
{count, plural,
|
||||
one { Image }
|
||||
other { Images }
|
||||
}
|
||||
harvesterhci.io.keypair: |-
|
||||
{count, plural,
|
||||
one { SSH Key }
|
||||
other { SSH Keys }
|
||||
}
|
||||
host: |-
|
||||
{count, plural,
|
||||
one { Host }
|
||||
other { Hosts }
|
||||
}
|
||||
configmap: |-
|
||||
{count, plural,
|
||||
one { Cloud Config Template }
|
||||
other { Cloud Config Templates }
|
||||
}
|
||||
k8s.cni.cncf.io.networkattachmentdefinition: |-
|
||||
{count, plural,
|
||||
one { Network }
|
||||
other { Networks }
|
||||
}
|
||||
cdi.kubevirt.io.datavolume: |-
|
||||
{count, plural,
|
||||
one { Volume }
|
||||
other { Volumes }
|
||||
}
|
||||
harvesterhci.io.user: |-
|
||||
{count, plural,
|
||||
one { User }
|
||||
other { Users }
|
||||
}
|
||||
harvesterhci.io.setting: |-
|
||||
{count, plural,
|
||||
one { Setting }
|
||||
other { Settings }
|
||||
}
|
||||
harvesterhci.io.virtualmachinetemplateversion: |-
|
||||
{count, plural,
|
||||
one { Template }
|
||||
other { Templates }
|
||||
}
|
||||
harvesterhci.io.virtualmachinebackup: |-
|
||||
{count, plural,
|
||||
one { Backup }
|
||||
other { Backups }
|
||||
}
|
||||
|
||||
|
||||
action:
|
||||
clone: Clone
|
||||
|
|
@ -5043,6 +5137,14 @@ advancedSettings:
|
|||
'ui-banners': 'Classification banner is used to display a custom fixed banner in the header, footer, or both.'
|
||||
'ui-default-landing': 'The default page users land on after login.'
|
||||
'brand': Folder name for an alternative theme defined in '/assets/brand'
|
||||
'vlan': Default physical NIC name of the VLAN network.
|
||||
'api-ui-source': Config how to load the API and UI source.
|
||||
'backup-target': Custom back target to store VM backups.
|
||||
'log-level': Configure Harvester server log level. Default to info.
|
||||
'rancher-enabled': Specify whether to display the Rancher UI navigation button or not.
|
||||
'server-version': Harvester server version.
|
||||
'upgrade-checker-enabled': Specify whether to enable Harvester upgrade check or not. Default is true.
|
||||
'upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to true.
|
||||
editHelp:
|
||||
'ui-banners': This setting takes a JSON object containing 3 root parameters; <code>banner</code>, <code>showHeader</code>, <code>showFooter</code>. <code>banner</code> is an object containing; <code>textColor</code>, <code>background</code>, and <code>text</code>, where <code>textColor</code> and <code>background</code> are any valid CSS color value.
|
||||
enum:
|
||||
|
|
@ -5057,6 +5159,14 @@ advancedSettings:
|
|||
dynamic: Dynamic
|
||||
true: Local
|
||||
false: Remote
|
||||
'api-ui-source':
|
||||
auto: 'Auto'
|
||||
bundled: 'Bundled'
|
||||
external: 'External'
|
||||
'log-level':
|
||||
info: Info
|
||||
debug: Debug
|
||||
trace: Trace
|
||||
|
||||
featureFlags:
|
||||
label: Feature Flags
|
||||
|
|
|
|||
|
|
@ -3142,6 +3142,8 @@ tableHeaders:
|
|||
value: 值
|
||||
version: 版本号
|
||||
weight: 权重
|
||||
progress: 进度
|
||||
fingerprint: 唯一标识
|
||||
|
||||
target:
|
||||
router:
|
||||
|
|
@ -3743,6 +3745,42 @@ workload:
|
|||
tip: 杀死 Pod 前所需的等待时间
|
||||
title: 升级中
|
||||
|
||||
harvester:
|
||||
dashboard:
|
||||
label: 概览
|
||||
host:
|
||||
label: 主机
|
||||
virtualMachine:
|
||||
label: 虚拟机
|
||||
volume:
|
||||
label: 卷
|
||||
image:
|
||||
label: 镜像
|
||||
vmTemplate:
|
||||
label: 虚拟机模板
|
||||
backup:
|
||||
label: 备份
|
||||
network:
|
||||
label: 网络
|
||||
sshKey:
|
||||
label: SSH 秘钥
|
||||
keypair: SSH 公钥
|
||||
tabs:
|
||||
basics: 基本信息
|
||||
|
||||
setting:
|
||||
label: 设置
|
||||
validation:
|
||||
physicalNIC: DefaultPhysicalNIC
|
||||
placeholder:
|
||||
accessKeyId: 指定您的访问密钥ID
|
||||
secretAccessKey: 指定您的私密访问密钥
|
||||
cert: 上传自签名SSL证书
|
||||
vlanChangeTip: 新修改的默认物理网卡仅适用于新增节点,不适用于现有节点
|
||||
defaultPhysicalNIC: 默认物理网卡
|
||||
|
||||
cloudTemplate:
|
||||
label: Cloud 配置模板
|
||||
|
||||
##############################
|
||||
# Model Properties
|
||||
|
|
@ -4001,6 +4039,17 @@ typeLabel:
|
|||
one { API密钥 }
|
||||
other { API密钥 }
|
||||
}
|
||||
kubevirt.io.virtualmachine: 虚拟机
|
||||
harvesterhci.io.virtualmachineimage: 镜像
|
||||
harvesterhci.io.keypair: SSH 密钥
|
||||
cloudTemplate: Cloud 配置模板
|
||||
harvesterhci.io.virtualmachinetemplateversion: 虚拟机模板
|
||||
k8s.cni.cncf.io.networkattachmentdefinition: 网络
|
||||
cdi.kubevirt.io.datavolume: 卷
|
||||
harvesterhci.io.user: 用户
|
||||
harvesterhci.io.setting: 设置
|
||||
host: 主机
|
||||
harvesterhci.io.virtualmachinebackup: 备份
|
||||
|
||||
action:
|
||||
clone: 克隆
|
||||
|
|
@ -4087,9 +4136,25 @@ advancedSettings:
|
|||
'rke-metadata-config': '配置RKE元数据刷新参数。'
|
||||
'ui-banners': '分类横幅是用来在页眉、页脚或两者中显示一个自定义的固定横幅。'
|
||||
'ui-default-landing': '用户在登录后登陆的默认页面。'
|
||||
'vlan': VLAN网络的默认物理NIC名称.
|
||||
'api-ui-source': 配置如何加载API和UI资源.
|
||||
'backup-target': 自定义备份目标用于存储VM备份.
|
||||
'log-level': 配置Harvester服务器日志级别. 默认为信息
|
||||
'rancher-enabled': 指定是否显示Rancher UI导航按钮.
|
||||
'server-version': Harvester服务器版本.
|
||||
'upgrade-checker-enabled': 指定是否启用Harvester升级检查. 默认为true.
|
||||
'upgrade-checker-url': 默认的Harvester升级检查地址。 仅在<code> upgrade-checker-enabled </code> 等于true时使用。
|
||||
editHelp:
|
||||
'ui-banners': 这个设置需要一个JSON对象,包含3个根参数;<code>banner</code>, <code>showHeader</code>, <code>showFooter</code>。<code>banner</code>是一个包含;<code>textColor</code>, <code>background</code>, 和<code>text</code>的对象,其中<code>textColor</code>和<code>background</code>是任何有效的CSS颜色值。
|
||||
#enum:
|
||||
enum:
|
||||
'api-ui-source':
|
||||
auto: 'Auto'
|
||||
bundled: 'Bundled'
|
||||
external: 'External'
|
||||
'log-level':
|
||||
info: Info
|
||||
debug: Debug
|
||||
trace: Trace
|
||||
#'ui-default-landing':
|
||||
# ember: Cluster Manager
|
||||
#vue: Cluster Explorer
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
type: [Number, String, undefined],
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="label">
|
||||
<div class="text-label">
|
||||
<slot name="name">
|
||||
{{ name }}
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="value">
|
||||
<slot name="value">
|
||||
{{ value }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tip">
|
||||
<span class="my-icon" :class="icon"></span>
|
||||
<span class="text">{{ text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tip {
|
||||
color: var(--disabled-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.my-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -79,6 +79,28 @@ export default {
|
|||
return sorted;
|
||||
},
|
||||
|
||||
harvesters() {
|
||||
const all = this.$store.getters['management/all'](MANAGEMENT.CLUSTER);
|
||||
let out = all.map((x) => {
|
||||
return {
|
||||
id: x.id,
|
||||
label: x.nameDisplay,
|
||||
ready: x.isReady,
|
||||
osLogo: x.providerOsLogo,
|
||||
logo: x.providerLogo,
|
||||
isLocal: x.isLocal
|
||||
};
|
||||
});
|
||||
|
||||
if (this.clusterFilter.length > 0) {
|
||||
out = out.filter(item => item.label.indexOf(this.clusterFilter) === 0);
|
||||
}
|
||||
|
||||
const sorted = sortBy(out, ['ready:desc', 'label']);
|
||||
|
||||
return sorted;
|
||||
},
|
||||
|
||||
dev: mapPref(DEV),
|
||||
|
||||
maxClustersToShow: mapPref(MENU_MAX_CLUSTERS),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,213 @@
|
|||
import { HCI, NODE, CONFIG_MAP } from '@/config/types';
|
||||
import {
|
||||
STATE, NAME as NAME_COL, AGE, NAMESPACE, IMAGE_DOWNLOAD_SIZE,
|
||||
FINGERPRINT
|
||||
} from '@/config/table-headers';
|
||||
|
||||
import { DSL } from '@/store/type-map';
|
||||
|
||||
export const NAME = 'virtual';
|
||||
|
||||
const TEMPLATE = HCI.VM_VERSION;
|
||||
const CLOUD_TEMPLATE = 'cloudTemplate';
|
||||
const HOST = 'host';
|
||||
|
||||
export function init(store) {
|
||||
const {
|
||||
product,
|
||||
basicType,
|
||||
headers,
|
||||
configureType,
|
||||
virtualType,
|
||||
} = DSL(store, NAME);
|
||||
|
||||
product({
|
||||
inStore: 'virtual',
|
||||
removable: false,
|
||||
showNamespaceFilter: true,
|
||||
showClusterSwitcher: false,
|
||||
icon: 'compass'
|
||||
});
|
||||
|
||||
basicType(['virtual-dashboard']);
|
||||
virtualType({
|
||||
ifHaveType: NODE,
|
||||
label: store.getters['i18n/t']('harvester.dashboard.label'),
|
||||
group: 'Root',
|
||||
labelDisplay: 'harvester.nav.dashboard',
|
||||
namespaced: true,
|
||||
name: 'virtual-dashboard',
|
||||
weight: 500,
|
||||
route: { name: 'c-cluster-virtual' },
|
||||
exact: true,
|
||||
});
|
||||
|
||||
configureType(HOST, {
|
||||
location: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: HOST },
|
||||
},
|
||||
resource: NODE,
|
||||
useCustomInImport: true
|
||||
});
|
||||
|
||||
configureType(HOST, { isCreatable: false, isEditable: true });
|
||||
basicType([HOST]);
|
||||
virtualType({
|
||||
ifHaveType: NODE,
|
||||
label: store.getters['i18n/t']('harvester.host.label'),
|
||||
group: 'Root',
|
||||
labelDisplay: 'harvester.typeLabel.host',
|
||||
name: HOST,
|
||||
namespaced: true,
|
||||
weight: 399,
|
||||
route: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: HOST }
|
||||
},
|
||||
exact: false,
|
||||
});
|
||||
|
||||
basicType([HCI.VM]);
|
||||
virtualType({
|
||||
label: store.getters['i18n/t']('harvester.virtualMachine.label'),
|
||||
group: 'root',
|
||||
name: HCI.VM,
|
||||
namespaced: true,
|
||||
weight: 299,
|
||||
route: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: HCI.VM }
|
||||
},
|
||||
exact: false,
|
||||
});
|
||||
|
||||
basicType([HCI.DATA_VOLUME]);
|
||||
virtualType({
|
||||
label: store.getters['i18n/t']('harvester.volume.label'),
|
||||
group: 'root',
|
||||
name: HCI.DATA_VOLUME,
|
||||
namespaced: true,
|
||||
weight: 199,
|
||||
route: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: HCI.DATA_VOLUME }
|
||||
},
|
||||
exact: false,
|
||||
});
|
||||
|
||||
basicType([HCI.IMAGE]);
|
||||
headers(HCI.IMAGE, [STATE, NAME_COL, NAMESPACE, /* IMAGE_PROGRESS, IMAGE_MESSAGE, */IMAGE_DOWNLOAD_SIZE, AGE]);
|
||||
virtualType({
|
||||
label: store.getters['i18n/t']('harvester.image.label'),
|
||||
group: 'root',
|
||||
name: HCI.IMAGE,
|
||||
namespaced: true,
|
||||
weight: 99,
|
||||
route: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: HCI.IMAGE }
|
||||
},
|
||||
exact: false,
|
||||
});
|
||||
|
||||
basicType([
|
||||
TEMPLATE,
|
||||
HCI.NETWORK_ATTACHMENT,
|
||||
HCI.BACKUP,
|
||||
HCI.SSH,
|
||||
CLOUD_TEMPLATE,
|
||||
HCI.SETTING
|
||||
], 'advanced');
|
||||
|
||||
configureType(HCI.CLUSTER_NETWORK, { realResource: HCI.SETTING, showState: false });
|
||||
virtualType({
|
||||
label: store.getters['i18n/t']('harvester.vmTemplate.label'),
|
||||
group: 'root',
|
||||
name: TEMPLATE,
|
||||
namespaced: true,
|
||||
weight: 289,
|
||||
route: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: TEMPLATE }
|
||||
},
|
||||
exact: false,
|
||||
});
|
||||
|
||||
configureType(HCI.BACKUP, {
|
||||
DisableEditInDetail: true,
|
||||
showListMasthead: false
|
||||
});
|
||||
virtualType({
|
||||
label: store.getters['i18n/t']('harvester.backup.label'),
|
||||
name: HCI.BACKUP,
|
||||
namespaced: true,
|
||||
weight: 200,
|
||||
route: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: HCI.BACKUP }
|
||||
},
|
||||
exact: false,
|
||||
});
|
||||
|
||||
configureType(HCI.NETWORK_ATTACHMENT, { isEditable: false, showState: false });
|
||||
virtualType({
|
||||
label: store.getters['i18n/t']('harvester.network.label'),
|
||||
name: HCI.NETWORK_ATTACHMENT,
|
||||
namespaced: true,
|
||||
weight: 189,
|
||||
route: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: HCI.NETWORK_ATTACHMENT }
|
||||
},
|
||||
exact: false,
|
||||
});
|
||||
|
||||
headers(HCI.SSH, [STATE, NAME_COL, NAMESPACE, FINGERPRINT, AGE]);
|
||||
virtualType({
|
||||
label: store.getters['i18n/t']('harvester.sshKey.label'),
|
||||
name: HCI.SSH,
|
||||
namespaced: true,
|
||||
weight: 170,
|
||||
route: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: HCI.SSH }
|
||||
},
|
||||
exact: false,
|
||||
});
|
||||
|
||||
configureType(CLOUD_TEMPLATE, {
|
||||
location: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: CLOUD_TEMPLATE },
|
||||
},
|
||||
resource: CONFIG_MAP,
|
||||
useCustomInImport: true
|
||||
});
|
||||
virtualType({
|
||||
label: store.getters['i18n/t']('harvester.cloudTemplate.label'),
|
||||
name: CLOUD_TEMPLATE,
|
||||
namespaced: true,
|
||||
weight: 87,
|
||||
route: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: CLOUD_TEMPLATE }
|
||||
},
|
||||
exact: false,
|
||||
});
|
||||
|
||||
// settings
|
||||
configureType(HCI.SETTING, { isCreatable: false });
|
||||
virtualType({
|
||||
ifHaveType: HCI.SETTING,
|
||||
label: store.getters['i18n/t']('harvester.setting.label'),
|
||||
name: HCI.SETTING,
|
||||
namespaced: true,
|
||||
weight: -1,
|
||||
route: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: HCI.SETTING }
|
||||
},
|
||||
exact: false
|
||||
});
|
||||
}
|
||||
|
|
@ -86,6 +86,47 @@ export const ALLOWED_SETTINGS = {
|
|||
},
|
||||
};
|
||||
|
||||
// harvester Settings ID
|
||||
const HCI_SETTING = {
|
||||
API_UI_SOURCE: 'api-ui-source',
|
||||
AUTH_TOKEN_MAX_TTL_MINUTES: 'auth-token-max-ttl-minutes',
|
||||
BACKUP_TARGET: 'backup-target',
|
||||
LOG_LEVEL: 'log-level',
|
||||
RANCHER_ENABLED: 'rancher-enabled',
|
||||
SERVER_URL: 'server-url',
|
||||
SERVER_VERSION: 'server-version',
|
||||
UI_INDEX: 'ui-index',
|
||||
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
|
||||
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
|
||||
VLAN: 'harvester-system/vlan',
|
||||
// DEFAULT_STORAGE_CLASS: 'default-storage-class'
|
||||
};
|
||||
|
||||
export const HCI_ALLOWED_SETTINGS = {
|
||||
[HCI_SETTING.API_UI_SOURCE]: {
|
||||
kind: 'enum',
|
||||
options: ['auto', 'external', 'bundled']
|
||||
},
|
||||
[HCI_SETTING.AUTH_TOKEN_MAX_TTL_MINUTES]: {},
|
||||
[HCI_SETTING.BACKUP_TARGET]: {
|
||||
kind: 'json', from: 'import', disableReset: true
|
||||
},
|
||||
[HCI_SETTING.LOG_LEVEL]: {
|
||||
kind: 'enum',
|
||||
options: ['info', 'debug', 'trace']
|
||||
},
|
||||
[HCI_SETTING.RANCHER_ENABLED]: { kind: 'boolean' },
|
||||
[HCI_SETTING.SERVER_VERSION]: {},
|
||||
[HCI_SETTING.SERVER_URL]: {},
|
||||
[HCI_SETTING.UI_INDEX]: { kind: 'url' },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||
[HCI_SETTING.VLAN]: {
|
||||
kind: 'custom', from: 'import', alias: 'vlan'
|
||||
},
|
||||
// [HCI_SETTING.DEFAULT_STORAGE_CLASS]: {}
|
||||
};
|
||||
|
||||
export const fetchOrCreateSetting = async(store, id, val, save = true) => {
|
||||
let setting;
|
||||
|
||||
|
|
|
|||
|
|
@ -831,3 +831,40 @@ export const STATE_NORMAN = {
|
|||
default: 'unknown',
|
||||
formatter: 'BadgeStateFormatter',
|
||||
};
|
||||
|
||||
/**
|
||||
* Harvester
|
||||
*/
|
||||
|
||||
// image
|
||||
export const IMAGE_DOWNLOAD_SIZE = {
|
||||
name: 'downloadedBytes',
|
||||
labelKey: 'tableHeaders.size',
|
||||
value: 'status.size',
|
||||
sort: 'status.size',
|
||||
formatter: 'ByteFormat',
|
||||
width: 120
|
||||
};
|
||||
|
||||
export const IMAGE_PROGRESS = {
|
||||
name: 'Uploaded',
|
||||
labelKey: 'tableHeaders.progress',
|
||||
value: 'status.progress',
|
||||
sort: 'status.progress',
|
||||
formatter: 'ImagePercentageBar',
|
||||
};
|
||||
|
||||
export const IMAGE_MESSAGE = {
|
||||
name: 'Message',
|
||||
labelKey: 'tableHeaders.message',
|
||||
value: 'status.conditions',
|
||||
sort: 'status.conditions',
|
||||
formatter: 'ImageMessage',
|
||||
};
|
||||
|
||||
// SSH keys
|
||||
export const FINGERPRINT = {
|
||||
name: 'Fingerprint',
|
||||
labelKey: 'tableHeaders.fingerprint',
|
||||
value: 'status.fingerPrint',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import Tabbed from '@/components/Tabbed';
|
||||
import Tab from '@/components/Tabbed/Tab';
|
||||
import LabelValue from '@/components/LabelValue';
|
||||
|
||||
export default {
|
||||
name: 'ViewSSH',
|
||||
|
||||
components: {
|
||||
LabelValue,
|
||||
Tab,
|
||||
Tabbed
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
let spec = this.value.spec;
|
||||
|
||||
if ( !this.value.spec ) {
|
||||
spec = {};
|
||||
this.value.spec = spec;
|
||||
this.value.metadata = { name: '' };
|
||||
}
|
||||
|
||||
return { publicKey: this.value.spec.publicKey || '' };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
|
||||
<Tab name="detail" :label="t('harvester.vmPage.detail.tabs.basics')" class="bordered-table">
|
||||
<div class="row">
|
||||
<div class="col span-12 keypair-card">
|
||||
<LabelValue name="SSH-Key" :value="publicKey" class="mb-20" />
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.keypair-card DIV {
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<script>
|
||||
import randomstring from 'randomstring';
|
||||
import Tabbed from '@/components/Tabbed';
|
||||
import Tab from '@/components/Tabbed/Tab';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import CruResource from '@/components/CruResource';
|
||||
import NameNsDescription from '@/components/form/NameNsDescription';
|
||||
import FileSelector, { createOnSelected } from '@/components/form/FileSelector';
|
||||
|
||||
import CreateEditView from '@/mixins/create-edit-view';
|
||||
|
||||
export default {
|
||||
name: 'EditSSH',
|
||||
|
||||
components: {
|
||||
Tab,
|
||||
Tabbed,
|
||||
CruResource,
|
||||
LabeledInput,
|
||||
FileSelector,
|
||||
NameNsDescription
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
if ( !this.value.spec ) {
|
||||
this.value.spec = {};
|
||||
this.value.metadata = { name: '' };
|
||||
}
|
||||
|
||||
return {
|
||||
publicKey: this.value.spec.publicKey || '',
|
||||
randomString: '',
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
publicKey(neu) {
|
||||
this.value.spec.publicKey = neu;
|
||||
|
||||
const splitSSH = neu.split(/\s+/);
|
||||
|
||||
if (splitSSH.length === 3) {
|
||||
if (splitSSH[2].includes('@') && !this.value.metadata.name) {
|
||||
this.value.metadata.name = splitSSH[2].split('@')[0];
|
||||
this.randomString = randomstring.generate(10).toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: { onKeySelected: createOnSelected('publicKey') },
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="keypair-card">
|
||||
<CruResource
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
@apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
>
|
||||
<div class="header mb-20">
|
||||
<FileSelector
|
||||
v-if="isCreate"
|
||||
class="btn btn-sm bg-primary mt-10"
|
||||
:label="t('generic.readFromFile')"
|
||||
accept=".pub"
|
||||
@selected="onKeySelected"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NameNsDescription
|
||||
ref="nd"
|
||||
:key="randomString"
|
||||
v-model="value"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
|
||||
<Tab name="basic" :label="t('harvester.sshKey.tabs.basics')" :weight="1" class="bordered-table">
|
||||
<LabeledInput
|
||||
v-model="publicKey"
|
||||
type="multiline"
|
||||
:mode="mode"
|
||||
:min-height="160"
|
||||
:label="t('harvester.sshKey.keypair')"
|
||||
required
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
<script>
|
||||
import CreateEditView from '@/mixins/create-edit-view';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import Password from '@/components/form/Password';
|
||||
import Tip from '@/components/Tip';
|
||||
|
||||
export default {
|
||||
name: 'BackupTarget',
|
||||
|
||||
components: {
|
||||
LabeledInput, LabeledSelect, Tip, Password
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
parseDefaultValue = JSON.parse(this.value.value);
|
||||
} catch (error) {
|
||||
parseDefaultValue = JSON.parse(this.value.default);
|
||||
}
|
||||
|
||||
if (!parseDefaultValue.type) {
|
||||
parseDefaultValue.type = 's3';
|
||||
}
|
||||
|
||||
return {
|
||||
parseDefaultValue,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
typeOption() {
|
||||
return [{
|
||||
value: 'nfs',
|
||||
label: 'NFS'
|
||||
}, {
|
||||
value: 's3',
|
||||
label: 'S3'
|
||||
}];
|
||||
},
|
||||
|
||||
virtualHostedStyleType() {
|
||||
return [{
|
||||
value: true,
|
||||
label: 'True'
|
||||
}, {
|
||||
value: false,
|
||||
label: 'False'
|
||||
}];
|
||||
},
|
||||
|
||||
isS3() {
|
||||
return this.parseDefaultValue.type === 's3';
|
||||
},
|
||||
|
||||
endpointPlaceholder() {
|
||||
return this.isS3 ? '' : 'nfs://server:/path/';
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'parseDefaultValue.type'(neu) {
|
||||
delete this.parseDefaultValue.accessKeyId;
|
||||
delete this.parseDefaultValue.secretAccessKey;
|
||||
delete this.parseDefaultValue.bucketName;
|
||||
delete this.parseDefaultValue.bucketRegion;
|
||||
delete this.parseDefaultValue.cert;
|
||||
delete this.parseDefaultValue.endpoint;
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.update();
|
||||
},
|
||||
|
||||
methods: {
|
||||
update() {
|
||||
const value = JSON.stringify(this.parseDefaultValue);
|
||||
|
||||
this.$set(this.value, 'value', value);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row" @input="update">
|
||||
<div class="col span-12">
|
||||
<LabeledSelect v-model="parseDefaultValue.type" class="mb-20" :label="t('harvester.fields.type')" :options="typeOption" @input="update" />
|
||||
|
||||
<LabeledInput v-model="parseDefaultValue.endpoint" class="mb-5" :placeholder="endpointPlaceholder" :mode="mode" label="Endpoint" />
|
||||
<Tip class="mb-20" icon="icons icon-h-question" :text="t('harvester.backUpPage.backupTargetTip')" />
|
||||
|
||||
<template v-if="isS3">
|
||||
<LabeledInput
|
||||
v-model="parseDefaultValue.bucketName"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
label="Bucket Name"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model="parseDefaultValue.bucketRegion"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
label="Bucket Region"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model="parseDefaultValue.accessKeyId"
|
||||
:placeholder="t('harvester.setting.placeholder.accessKeyId')"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
label="Access Key ID"
|
||||
required
|
||||
/>
|
||||
|
||||
<Password
|
||||
v-model="parseDefaultValue.secretAccessKey"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.setting.placeholder.secretAccessKey')"
|
||||
label="Secret Access Key"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model="parseDefaultValue.cert"
|
||||
type="multiline"
|
||||
class="mb-20"
|
||||
:placeholder="t('harvester.setting.placeholder.cert')"
|
||||
:mode="mode"
|
||||
:min-height="120"
|
||||
label="Certificate"
|
||||
/>
|
||||
|
||||
<LabeledSelect v-model="parseDefaultValue.virtualHostedStyle" class="mb-20" label="Virtual Hosted-Style" :options="virtualHostedStyleType" @input="update" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.icon-h-question {
|
||||
font-size: 24px;
|
||||
}
|
||||
.tip {
|
||||
font-size: 15px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
<script>
|
||||
import CruResource from '@/components/CruResource';
|
||||
import RadioGroup from '@/components/form/RadioGroup';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import TextAreaAutoGrow from '@/components/form/TextAreaAutoGrow';
|
||||
|
||||
import CreateEditView from '@/mixins/create-edit-view';
|
||||
|
||||
import { HCI_ALLOWED_SETTINGS } from '@/config/settings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CruResource,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup,
|
||||
TextAreaAutoGrow
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
data() {
|
||||
const t = this.$store.getters['i18n/t'];
|
||||
const setting = HCI_ALLOWED_SETTINGS[this.value.id];
|
||||
|
||||
let enumOptions = [];
|
||||
|
||||
if (setting.kind === 'enum' ) {
|
||||
enumOptions = setting.options.map(id => ({
|
||||
label: `advancedSettings.enum.${ this.value.id }.${ id }`,
|
||||
value: id,
|
||||
}));
|
||||
}
|
||||
|
||||
const canReset = !setting.disableReset && (!!this.value.default || this.value.canReset);
|
||||
|
||||
if (this.value.value === undefined) {
|
||||
this.$set(this.value, 'value', null);
|
||||
}
|
||||
|
||||
this.value.value = this.value.value || this.value.default;
|
||||
|
||||
return {
|
||||
setting,
|
||||
description: t(`advancedSettings.descriptions.${ this.value.id }`),
|
||||
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
||||
enumOptions,
|
||||
canReset,
|
||||
errors: [],
|
||||
hasCustomComponent: false,
|
||||
customComponent: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
doneLocationOverride() {
|
||||
return this.value.doneOverride;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
let customComponent = false;
|
||||
|
||||
const resource = this.$route.params.resource;
|
||||
const name = this.value.metadata.name;
|
||||
const path = `${ resource }/${ name }`;
|
||||
|
||||
const hasCustomComponent = this.$store.getters['type-map/haveComponent'](path);
|
||||
|
||||
if ( hasCustomComponent ) {
|
||||
customComponent = this.$store.getters['type-map/importComponent'](path);
|
||||
}
|
||||
this.hasCustomComponent = hasCustomComponent;
|
||||
this.customComponent = customComponent;
|
||||
},
|
||||
|
||||
methods: {
|
||||
saveSettings(done) {
|
||||
const t = this.$store.getters['i18n/t'];
|
||||
|
||||
// Validate the JSON if the setting is a json value
|
||||
if (this.setting.kind === 'json') {
|
||||
try {
|
||||
JSON.parse(this.value.value);
|
||||
this.errors = [];
|
||||
} catch (e) {
|
||||
this.errors = [t('advancedSettings.edit.invalidJSON')];
|
||||
|
||||
return done(false);
|
||||
}
|
||||
}
|
||||
|
||||
this.save(done);
|
||||
},
|
||||
|
||||
useDefault(ev) {
|
||||
// Lose the focus on the button after click
|
||||
if (ev && ev.srcElement) {
|
||||
ev.srcElement.blur();
|
||||
}
|
||||
|
||||
if (this.value.default) {
|
||||
this.value.value = this.value.default;
|
||||
} else {
|
||||
this.value = this.value.defaultValue;
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
class="route"
|
||||
:done-route="'c-cluster-product-resource'"
|
||||
:errors="errors"
|
||||
:mode="mode"
|
||||
:resource="value"
|
||||
:subtypes="[]"
|
||||
:can-yaml="false"
|
||||
@error="e=>errors = e"
|
||||
@finish="saveSettings"
|
||||
@cancel="done"
|
||||
>
|
||||
<h4>{{ description }}</h4>
|
||||
|
||||
<h5 v-if="editHelp" class="edit-help" v-html="editHelp" />
|
||||
|
||||
<div class="edit-change mt-20">
|
||||
<h5 v-t="'advancedSettings.edit.changeSetting'" />
|
||||
<button :disabled="!canReset" type="button" class="btn role-primary" @click="useDefault">
|
||||
{{ t('advancedSettings.edit.useDefault') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-20">
|
||||
<div v-if="setting.from === 'import'">
|
||||
<component
|
||||
:is="customComponent"
|
||||
v-if="hasCustomComponent"
|
||||
v-model="value"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="setting.kind === 'enum'">
|
||||
<LabeledSelect
|
||||
v-model="value.value"
|
||||
:label="t('advancedSettings.edit.value')"
|
||||
:localized-label="true"
|
||||
:mode="mode"
|
||||
:options="enumOptions"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="setting.kind === 'boolean'">
|
||||
<RadioGroup
|
||||
v-model="value.value"
|
||||
name="settings_value"
|
||||
:labels="[t('advancedSettings.edit.trueOption'), t('advancedSettings.edit.falseOption')]"
|
||||
:options="['true', 'false']"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="setting.kind === 'multiline' || setting.kind === 'json'">
|
||||
<TextAreaAutoGrow
|
||||
v-model="value.value"
|
||||
:min-height="254"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<LabeledInput
|
||||
v-model="value.value"
|
||||
:label="t('advancedSettings.edit.value')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-change {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
> h5 {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .edit-help code {
|
||||
padding: 1px 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<script>
|
||||
import Setting from '@/edit/harvesterhci.io.setting';
|
||||
|
||||
export default {
|
||||
name: 'EditClusterNetwork',
|
||||
components: { Setting },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Setting :value="value" />
|
||||
</template>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import RadioGroup from '@/components/form/RadioGroup';
|
||||
import Tip from '@/components/Tip';
|
||||
import CreateEditView from '@/mixins/create-edit-view';
|
||||
|
||||
export default {
|
||||
name: 'EditVlan',
|
||||
components: {
|
||||
LabeledInput,
|
||||
RadioGroup,
|
||||
Tip
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
if (!this.value.config) {
|
||||
Vue.set(this.value, 'config', { defaultPhysicalNIC: '' });
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
doneLocationOverride() {
|
||||
return this.value.listLocation;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<RadioGroup
|
||||
v-model="value.enable"
|
||||
class="mb-20"
|
||||
name="model"
|
||||
:options="[true,false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-if="value.enable"
|
||||
v-model="value.config.defaultPhysicalNIC"
|
||||
:label="t('harvester.setting.defaultPhysicalNIC')"
|
||||
class="mb-5"
|
||||
/>
|
||||
|
||||
<Tip v-if="value.enable" icon="icons icon-h-question" :text="t('harvester.setting.vlanChangeTip')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .radio-group {
|
||||
display: flex;
|
||||
.radio-container {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { HCI } from '@/config/types';
|
||||
import { HCI_ALLOWED_SETTINGS } from '@/config/settings';
|
||||
import { allHash } from '@/utils/promise';
|
||||
import Banner from '@/components/Banner';
|
||||
import Loading from '@/components/Loading';
|
||||
import { DEV } from '@/store/prefs';
|
||||
|
||||
export default {
|
||||
components: { Banner, Loading },
|
||||
|
||||
async fetch() {
|
||||
const isDev = this.$store.getters['prefs/get'](DEV);
|
||||
const rows = await allHash({
|
||||
clusterNetwork: this.$store.dispatch('cluster/findAll', { type: HCI.CLUSTER_NETWORK }),
|
||||
haversterSettings: this.$store.dispatch('cluster/findAll', { type: HCI.SETTING }),
|
||||
});
|
||||
|
||||
const allRows = [...rows.clusterNetwork, ...rows.haversterSettings];
|
||||
|
||||
// Map settings from array to object keyed by id
|
||||
const settingsMap = allRows.reduce((res, s) => {
|
||||
res[s.id] = s;
|
||||
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
const initSettings = [];
|
||||
|
||||
// Combine the allowed settings with the data from the API
|
||||
Object.keys(HCI_ALLOWED_SETTINGS).forEach((setting) => {
|
||||
if (!settingsMap[setting]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const realSetting = HCI_ALLOWED_SETTINGS[setting]?.alias || setting;
|
||||
const s = {
|
||||
...HCI_ALLOWED_SETTINGS[setting],
|
||||
id: realSetting,
|
||||
data: settingsMap[setting],
|
||||
};
|
||||
|
||||
s.hide = s.canHide = (s.kind === 'json' || s.kind === 'multiline');
|
||||
|
||||
// There are only 2 actions that can be enabled - Edit Setting or View in API
|
||||
// If neither is available for this setting then we hide the action menu button
|
||||
s.hasActions = !s.readOnly || isDev;
|
||||
initSettings.push(s);
|
||||
});
|
||||
|
||||
this.initSettings = initSettings;
|
||||
},
|
||||
|
||||
data() {
|
||||
return { initSettings: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
settings() {
|
||||
return this.initSettings.map((setting) => {
|
||||
const s = setting;
|
||||
|
||||
if (s.kind === 'json') {
|
||||
s.json = JSON.stringify(JSON.parse(s.data.value || s.data.default), null, 2);
|
||||
} else if (s.kind === 'enum') {
|
||||
const v = s.data.value || s.data.default;
|
||||
|
||||
s.enum = `advancedSettings.enum.${ s.id }.${ v }`;
|
||||
} else if (s.kind === 'custom') {
|
||||
s.custom = s.data.customValue;
|
||||
}
|
||||
|
||||
return {
|
||||
...s,
|
||||
description: this.t(`advancedSettings.descriptions.${ s.id }`),
|
||||
customized: (!s.readonly && s.data.value && s.data.value !== s.data.default) || s.data.hasCustomized
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
showActionMenu(e, setting) {
|
||||
const actionElement = e.srcElement;
|
||||
|
||||
this.$store.commit(`action-menu/show`, {
|
||||
resources: setting.data,
|
||||
elem: actionElement
|
||||
});
|
||||
},
|
||||
|
||||
getSettingOption(id) {
|
||||
return HCI_ALLOWED_SETTINGS.find(setting => setting.id === id);
|
||||
},
|
||||
|
||||
toogleHide(s) {
|
||||
this.initSettings.find((setting) => {
|
||||
if (setting.id === s.id) {
|
||||
setting.hide = !setting.hide;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="!settings" />
|
||||
<div v-else>
|
||||
<Banner color="warning" class="settings-banner">
|
||||
<div>
|
||||
{{ t('advancedSettings.subtext') }}
|
||||
</div>
|
||||
</Banner>
|
||||
<div v-for="setting in settings" :key="setting.id" class="advanced-setting mb-20">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<h1>{{ setting.id }}<span v-if="setting.customized" class="modified">Modified</span></h1>
|
||||
<h2>{{ setting.description }}</h2>
|
||||
</div>
|
||||
<div v-if="setting.hasActions" class="action">
|
||||
<button aria-haspopup="true" aria-expanded="false" type="button" class="btn btn-sm role-multi-action actions" @click="showActionMenu($event, setting)">
|
||||
<i class="icon icon-actions" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div value>
|
||||
<div v-if="setting.hide">
|
||||
<button class="btn btn-sm role-primary" @click="toogleHide(setting)">
|
||||
{{ t('advancedSettings.show') }} {{ setting.id }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="settings-value">
|
||||
<pre v-if="setting.kind === 'json'">{{ setting.json }}</pre>
|
||||
<pre v-else-if="setting.kind === 'multiline'">{{ setting.data.value || setting.data.default }}</pre>
|
||||
<pre v-else-if="setting.kind === 'enum'">{{ t(setting.enum) }}</pre>
|
||||
<pre v-else-if="setting.kind === 'custom' && setting.custom"> {{ setting.custom }}</pre>
|
||||
<pre v-else-if="setting.data.value || setting.data.default">{{ setting.data.value || setting.data.default }}</pre>
|
||||
<pre v-else class="text-muted"><{{ t('advancedSettings.none') }}></pre>
|
||||
</div>
|
||||
<div v-if="setting.canHide && !setting.hide" class="mt-5">
|
||||
<button class="btn btn-sm role-primary" @click="toogleHide(setting)">
|
||||
{{ t('advancedSettings.hide') }} {{ setting.id }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Banner v-if="setting.data.errMessage" color="error mt-5" class="settings-banner">
|
||||
{{ setting.data.errMessage }}
|
||||
</Banner>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.settings-banner {
|
||||
margin-top: 0;
|
||||
}
|
||||
.advanced-setting {
|
||||
border: 1px solid var(--border);
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-value pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modified {
|
||||
margin-left: 10px;
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 5px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import { findBy } from '@/utils/array';
|
||||
import { HCI_ALLOWED_SETTINGS } from '@/config/settings';
|
||||
|
||||
export default {
|
||||
_availableActions() {
|
||||
const out = this._standardActions;
|
||||
|
||||
const toFilter = ['cloneYaml', 'download', 'goToEditYaml', 'goToViewYaml', 'goToViewConfig', 'promptRemove'];
|
||||
|
||||
const actions = out.map((O) => {
|
||||
const enabled = toFilter.includes(O.action) ? false : O.enabled;
|
||||
const bulkable = toFilter.includes(O.action) ? false : O?.bulkable;
|
||||
|
||||
return {
|
||||
...O,
|
||||
enabled,
|
||||
bulkable
|
||||
};
|
||||
});
|
||||
|
||||
const editAction = actions.find(action => action.action === 'goToEdit');
|
||||
|
||||
if (editAction) {
|
||||
editAction.label = this.t('advancedSettings.edit.label');
|
||||
}
|
||||
|
||||
return actions;
|
||||
},
|
||||
|
||||
hasCustomized() {
|
||||
const setting = HCI_ALLOWED_SETTINGS[this.id];
|
||||
const readonly = !!setting.readOnly;
|
||||
|
||||
return !readonly && this.value && this.value !== this.default;
|
||||
},
|
||||
|
||||
backupTagetetIsEmpty() {
|
||||
return !this.value;
|
||||
},
|
||||
|
||||
errMessage() {
|
||||
if (this.metadata?.state?.error === true) {
|
||||
return this.metadata.state.message;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
configuredCondition() {
|
||||
return findBy((this?.status?.conditions || []), 'type', 'configured') || {};
|
||||
},
|
||||
|
||||
valueOrDefaultValue() {
|
||||
return this.value || this.default;
|
||||
},
|
||||
|
||||
upgradeableVersion() {
|
||||
const value = this.value || '';
|
||||
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value.split(',').sort((a, b) => {
|
||||
return a > b ? -1 : 1;
|
||||
}).map( (V) => {
|
||||
return {
|
||||
label: V,
|
||||
value: V
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
currentVersion() {
|
||||
return this.value || '';
|
||||
},
|
||||
|
||||
displayValue() { // Select the field you want to display
|
||||
if (this.id === 'backup-target') {
|
||||
return this.parseValue?.endpoint || ' ';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
parseValue() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
parseDefaultValue = JSON.parse(this.value);
|
||||
} catch (err) {
|
||||
parseDefaultValue = JSON.parse(this.default);
|
||||
}
|
||||
|
||||
return parseDefaultValue;
|
||||
},
|
||||
|
||||
isS3() {
|
||||
return this.parseValue.type === 's3';
|
||||
},
|
||||
|
||||
isNFS() {
|
||||
return this.parseValue.type === 'nfs';
|
||||
},
|
||||
|
||||
customValidationRules() {
|
||||
const id = this.id;
|
||||
|
||||
const out = [];
|
||||
|
||||
switch (id) {
|
||||
case 'backup-target':
|
||||
out.push( {
|
||||
nullable: false,
|
||||
path: 'value',
|
||||
required: true,
|
||||
type: 'string',
|
||||
validators: ['backupTarget'],
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { HCI } from '@/config/types';
|
||||
import { clone } from '@/utils/object';
|
||||
|
||||
export default {
|
||||
availableActions() {
|
||||
let out = this._standardActions;
|
||||
const toFilter = ['goToClone', 'cloneYaml', 'goToViewYaml', 'goToViewConfig', 'promptRemove', 'goToEditYaml', 'download'];
|
||||
|
||||
out = out.filter((action) => {
|
||||
if (!toFilter.includes(action.action)) {
|
||||
return action;
|
||||
}
|
||||
});
|
||||
|
||||
const editAction = out.find(action => action.action === 'goToEdit');
|
||||
|
||||
if (editAction) {
|
||||
editAction.label = this.t('advancedSettings.edit.label');
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
doneOverride() {
|
||||
const detailLocation = clone(this.listLocation);
|
||||
|
||||
detailLocation.params.resource = HCI.SETTING;
|
||||
|
||||
return detailLocation;
|
||||
},
|
||||
|
||||
// vlan
|
||||
canUseVlan() {
|
||||
return this.isVlanOpen && this.defaultPhysicalNic.length > 0;
|
||||
},
|
||||
|
||||
canReset() {
|
||||
return true;
|
||||
},
|
||||
|
||||
defaultValue() {
|
||||
this.enable = false;
|
||||
if (this.config) { // initializing: the config value is empty
|
||||
this.config.defaultPhysicalNIC = '';
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
isVlanOpen() {
|
||||
return !!this.enable;
|
||||
},
|
||||
|
||||
defaultPhysicalNic() {
|
||||
return this?.config?.defaultPhysicalNIC;
|
||||
},
|
||||
|
||||
displayValue() { // Select the field you want to display
|
||||
if (this.enable) {
|
||||
return this?.config?.defaultPhysicalNIC || '';
|
||||
}
|
||||
},
|
||||
|
||||
customValue() {
|
||||
return this.enable ? this?.config?.defaultPhysicalNIC : null;
|
||||
},
|
||||
|
||||
hasCustomized() {
|
||||
return this.enable;
|
||||
},
|
||||
|
||||
customValidationRules() {
|
||||
const out = [];
|
||||
|
||||
if (this.enable) {
|
||||
out.push({
|
||||
nullable: false,
|
||||
path: 'config.defaultPhysicalNIC',
|
||||
required: true,
|
||||
translationKey: 'harvester.setting.validation.physicalNIC',
|
||||
type: 'string',
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
};
|
||||
|
|
@ -72,6 +72,7 @@
|
|||
"nuxt": "2.14.6",
|
||||
"papaparse": "^5.3.0",
|
||||
"portal-vue": "^2.1.5",
|
||||
"randomstring": "^1.2.1",
|
||||
"require-extension-hooks": "^0.3.3",
|
||||
"require-extension-hooks-babel": "^1.0.0-beta.1",
|
||||
"require-extension-hooks-vue": "^3.0.0",
|
||||
|
|
|
|||
|
|
@ -28,9 +28,8 @@ export const plugins = [
|
|||
namespace: 'management', baseUrl: '/v1', modelBaseClass: BY_TYPE
|
||||
}),
|
||||
Steve({ namespace: 'cluster', baseUrl: '' }), // URL dynamically set for the selected cluster
|
||||
Steve({
|
||||
namespace: 'rancher', baseUrl: '/v3', modelBaseClass: NORMAN_CLASS
|
||||
}),
|
||||
Steve({ namespace: 'rancher', baseUrl: '/v3' }),
|
||||
Steve({ namespace: 'virtual', baseUrl: '' }),
|
||||
];
|
||||
|
||||
export const state = () => {
|
||||
|
|
@ -587,12 +586,15 @@ export const actions = {
|
|||
}
|
||||
|
||||
const clusterBase = `/k8s/clusters/${ escape(id) }/v1`;
|
||||
const virtualBase = `/k8s/clusters/${ escape(id) }/v1/harvester`;
|
||||
|
||||
// Update the Steve client URLs
|
||||
commit('cluster/applyConfig', { baseUrl: clusterBase });
|
||||
commit('virtual/applyConfig', { baseUrl: virtualBase });
|
||||
|
||||
await Promise.all([
|
||||
dispatch('cluster/loadSchemas', true),
|
||||
dispatch('virtual/loadSchemas', true),
|
||||
]);
|
||||
|
||||
dispatch('cluster/subscribe');
|
||||
|
|
@ -606,10 +608,12 @@ export const actions = {
|
|||
};
|
||||
|
||||
const res = await allHash({
|
||||
projects: isRancher && dispatch('management/findAll', projectArgs),
|
||||
counts: dispatch('cluster/findAll', { type: COUNT }),
|
||||
namespaces: dispatch('cluster/findAll', { type: NAMESPACE }),
|
||||
navLinks: !!getters['cluster/schemaFor'](UI.NAV_LINK) && dispatch('cluster/findAll', { type: UI.NAV_LINK }),
|
||||
projects: isRancher && dispatch('management/findAll', projectArgs),
|
||||
counts: dispatch('cluster/findAll', { type: COUNT }),
|
||||
virtualCount: dispatch('virtual/findAll', { type: COUNT }),
|
||||
namespaces: dispatch('cluster/findAll', { type: NAMESPACE }),
|
||||
virtualNamespaces: dispatch('virtual/findAll', { type: NAMESPACE }),
|
||||
navLinks: !!getters['cluster/schemaFor'](UI.NAV_LINK) && dispatch('cluster/findAll', { type: UI.NAV_LINK }),
|
||||
});
|
||||
|
||||
await dispatch('cleanNamespaces');
|
||||
|
|
|
|||
|
|
@ -118,7 +118,9 @@ import { clone, get } from '@/utils/object';
|
|||
import {
|
||||
ensureRegex, escapeHtml, escapeRegex, ucFirst, pluralize
|
||||
} from '@/utils/string';
|
||||
import { importList, importDetail, importEdit, loadProduct } from '@/utils/dynamic-importer';
|
||||
import {
|
||||
importList, importDetail, importEdit, loadProduct, importComponent
|
||||
} from '@/utils/dynamic-importer';
|
||||
|
||||
import { NAME as EXPLORER } from '@/config/product/explorer';
|
||||
import isObject from 'lodash/isObject';
|
||||
|
|
@ -995,6 +997,24 @@ export const getters = {
|
|||
};
|
||||
},
|
||||
|
||||
haveComponent(state, getters) {
|
||||
return (path) => {
|
||||
try {
|
||||
require.resolve(`@/edit/${ path }`);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
importComponent(state, getters) {
|
||||
return (path) => {
|
||||
return importComponent(path);
|
||||
};
|
||||
},
|
||||
|
||||
importList(state, getters) {
|
||||
return (rawType) => {
|
||||
const type = getters.componentFor(rawType);
|
||||
|
|
|
|||
|
|
@ -83,3 +83,11 @@ export function loadTranslation(name) {
|
|||
// Note: directly returns the import, not a function
|
||||
return import(/* webpackChunkName: "[request]" */ `@/assets/translations/${name}.yaml`);
|
||||
}
|
||||
|
||||
export function importComponent(path) {
|
||||
if ( !path ) {
|
||||
throw new Error('Path required');
|
||||
}
|
||||
|
||||
return () => import(/* webpackChunkName: "import-component" */ `@/edit/${ path }`);
|
||||
}
|
||||
13
yarn.lock
13
yarn.lock
|
|
@ -4311,6 +4311,11 @@ array-union@^2.1.0:
|
|||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
||||
|
||||
array-uniq@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.nlark.com/array-uniq/download/array-uniq-1.0.2.tgz?cache=0&sync_timestamp=1620042102127&other_urls=https%3A%2F%2Fregistry.nlark.com%2Farray-uniq%2Fdownload%2Farray-uniq-1.0.2.tgz#5fcc373920775723cfd64d65c64bef53bf9eba6d"
|
||||
integrity sha1-X8w3OSB3VyPP1k1lxkvvU7+eum0=
|
||||
|
||||
array-uniq@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
|
||||
|
|
@ -12359,6 +12364,14 @@ randomfill@^1.0.3:
|
|||
randombytes "^2.0.5"
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
randomstring@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.nlark.com/randomstring/download/randomstring-1.2.1.tgz#71cd3cda24ad1b7e0b65286b3aa5c10853019349"
|
||||
integrity sha1-cc082iStG34LZShrOqXBCFMBk0k=
|
||||
dependencies:
|
||||
array-uniq "1.0.2"
|
||||
randombytes "2.0.3"
|
||||
|
||||
range-parser@^1.2.1, range-parser@~1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
|
|
|
|||
Loading…
Reference in New Issue