dashboard/shell/components/ConfigMapSettings/index.vue

355 lines
8.1 KiB
Vue

<script lang="ts">
import { PropType } from 'vue';
import jsyaml from 'js-yaml';
import { merge } from 'lodash';
import { set, get } from '@shell/utils/object';
import YAML from 'yaml';
import { CONFIG_MAP, MANAGEMENT } from '@shell/config/types';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { toSeconds } from '@shell/utils/duration';
import Loading from '@shell/components/Loading.vue';
import AsyncButton from '@shell/components/AsyncButton.vue';
import { Banner } from '@components/Banner';
import Settings from '@shell/components/ConfigMapSettings/Settings.vue';
export type SettingType = 'string' | 'number' | 'boolean' | 'array' | 'object';
export type SettingHandler = 'Textarea' | 'KeyValue' | 'Taints' | 'UnitInput';
export interface Item {
type: SettingType,
value: string
}
export interface Group {
name: string,
children: string[],
expanded?: boolean,
weight: number,
}
export interface Setting {
path: string,
type: SettingType,
handler?: SettingHandler,
items?: Item[],
default: object | string | boolean | number,
tooltip?: boolean,
info?: boolean,
placeholder?: boolean,
class?: string,
weight: number,
// validationRules,
}
interface DataType {
noPermissions: boolean,
configMap: object | null,
values: object,
errors: string[]
valuesErrors: string[],
}
export default {
name: 'ConfigMapSettings',
emits: ['done', 'errors'],
components: {
Loading,
AsyncButton,
Banner,
Settings,
},
props: {
/**
* A key-value object that defines a flat list of settings.
* Each entry describe the setting name and the path where to get/set in the ConfigMap data field
*
* example:
*
* {
* setting-name: {
* weight: 0,
* type: 'number',
* path: 'the.path.to.nested.value',
* default: '50',
* tooltip: false,
* info: false,
* },
* ...
* }
*/
settings: {
type: Object as PropType<Record<string, Setting>>,
required: true
},
/**
* Groups of Settings
*/
groups: {
type: Array as PropType<Group[]>,
default: () => [],
},
/**
* ConfigMap name
*/
name: {
type: String,
required: true
},
/**
* ConfigMap namespace
*/
namespace: {
type: String,
required: true
},
/**
* The key in ConfigMap.data where to save the settings values
*/
dataKey: {
type: String,
required: true
},
inStore: {
type: String,
default: 'cluster',
},
labelKeyPrefix: {
type: String,
default: 'settings'
},
showDescription: {
type: Boolean,
default: false
},
showInfo: {
type: Boolean,
default: false
},
},
async fetch() {
if (this.$store.getters[`${ this.inStore }/schemaFor`](CONFIG_MAP)) {
try {
this.configMap = await this.$store.dispatch(`${ this.inStore }/find`, { type: CONFIG_MAP, id: `${ this.namespace }/${ this.name }` });
} catch (err) {
}
this.initValues();
} else {
this.noPermissions = true;
}
},
data(): DataType {
return {
noPermissions: false,
configMap: null,
values: {},
errors: [],
valuesErrors: []
};
},
computed: {
mode() {
if (this.valuesErrors.length) {
return _VIEW;
}
const settingsSchema = this.$store.getters[`${ this.inStore }/schemaFor`](MANAGEMENT.SETTING);
const configMapsSchema = this.$store.getters[`${ this.inStore }/schemaFor`](CONFIG_MAP);
if (settingsSchema?.resourceMethods?.includes('PUT') && configMapsSchema?.resourceMethods?.includes('PUT')) {
return _EDIT;
}
return _VIEW;
},
canEdit() {
return this.mode === _EDIT;
},
fetchState(): {pending: boolean} {
return (this as any).$fetchState;
},
},
methods: {
async save(btnCB: (arg: boolean) => void) {
const configMap = this.configMap || await this.$store.dispatch(`${ this.inStore }/create`, {
type: CONFIG_MAP,
metadata: {
namespace: this.namespace,
name: this.name
}
});
if (!configMap.data) {
configMap.data = {};
}
try {
const configMapValues = YAML.parse(configMap.data[this.dataKey] || '');
const currentValues = this.buildValues(this.values, this.encodeValue);
const values = merge(configMapValues, currentValues);
configMap.data[this.dataKey] = jsyaml.dump(values);
await configMap.save();
this.$emit('done', configMap);
btnCB(true);
} catch (err) {
this.errors.push(err as string);
this.$emit('errors', this.errors);
btnCB(false);
}
},
initValues() {
try {
const configMapValues = get(this.configMap || {}, `data.${ this.dataKey }`);
const currentValues = YAML.parse(configMapValues || '');
this.values = this.buildValues(currentValues, this.decodeValue);
} catch (err) {
const msg = this.t(`${ this.labelKeyPrefix }.parseError`, { id: `${ this.namespace }/${ this.name }`, path: `data.${ this.dataKey }` }, true);
this.valuesErrors.push(msg);
}
},
buildValues<T = object | string | boolean | number>(values: object, fn: (name: string, value: T) => T) {
return Object.keys(this.settings).reduce((acc, name) => {
const value = get(values, this.settings[name].path);
set(acc, this.settings[name].path, fn(name, value));
return acc;
}, {});
},
decodeValue<T = object | string | boolean | number>(name: string, value: T): T {
// use default value
if (value === undefined) {
value = this.settings[name].default as T;
}
// object types to json
if (this.settings[name].type === 'object' && this.settings[name].handler === 'Textarea') {
value = JSON.stringify(value || {}) as T;
}
// number in seconds
if (this.settings[name].type === 'number' && this.settings[name].handler === 'UnitInput') {
value = toSeconds(value) as T;
}
return value;
},
encodeValue<T = object | string | boolean | number>(name: string, value: T): T {
// object types from json
if (this.settings[name].type === 'object' && this.settings[name].handler === 'Textarea') {
value = YAML.parse(value as string);
}
// number to string with unit of measure
if (this.settings[name].type === 'number' && this.settings[name].handler === 'UnitInput') {
value = `${ value || 0 }s` as T;
}
return value;
}
}
};
</script>
<template>
<Loading v-if="fetchState.pending" />
<div v-else-if="noPermissions">
<slot name="no-permissions">
<span>
{{ t(`${ labelKeyPrefix }.noPermissions`) }}
</span>
</slot>
</div>
<div
v-else
data-testid="cm-settings"
>
<slot name="header">
<div class="header">
<h1>
{{ t(`${ labelKeyPrefix }.title`) }}
</h1>
<label
v-if="showDescription"
class="text-label"
>
{{ t(`${ labelKeyPrefix }.description`, {}, true) }}
</label>
</div>
</slot>
<slot name="info">
<Banner
v-if="showInfo"
color="info"
:label="t(`${ labelKeyPrefix }.info`, {}, true)"
/>
</slot>
<slot name="errors">
<template
v-for="(err, j) in [ ...valuesErrors, ...errors ]"
:key="j"
>
<Banner
color="error"
data-testid="cm-settings-error"
:label="err"
/>
</template>
</slot>
<Settings
class="mt-10"
:settings="settings"
:groups="groups"
:values="values"
:mode="mode"
:label-key-prefix="labelKeyPrefix"
@update:value="values=$event"
/>
<div v-if="canEdit">
<AsyncButton
class="pull-right mt-30"
:action-label="t(`${ labelKeyPrefix }.apply`)"
@click="save"
/>
</div>
</div>
</template>
<style scoped lang='scss'>
</style>