jcloud/dashboard/src2/components/ConfigEditor.vue

342 lines
8.0 KiB
Vue

<template>
<Card :title="title || 'Site Config'">
<template #actions>
<Button
class="mr-2"
:loading="$resources.configData.loading"
v-if="isDirty"
@click="
() => {
$resources.configData.reload().then(() => {
isDirty = false;
});
}
"
>
Discard changes
</Button>
<Button
variant="solid"
v-if="isDirty"
@click="updateConfig"
:loading="$resources.updateConfig.loading"
>
Save changes
</Button>
</template>
<div class="flex space-x-4">
<div class="w-full shrink-0 space-y-4 md:w-1/2">
<div class="ml-2">
<ErrorMessage :message="$resources.updateConfig.error" />
<div
v-if="$resources.configData?.data?.length"
v-for="config in $resources.configData.data"
:key="config.key"
class="mt-2 flex"
>
<FormControl
:label="getStandardConfigTitle(config.key)"
v-model="config.value"
@click="
config.type === 'Password' ? (config.value = '') : null;
config.type === 'Password' ? (isDirty = true) : null;
"
@input="isDirty = true"
class="flex-1"
/>
<Button
class="ml-2 mt-5"
icon="x"
variant="ghost"
@click="removeConfig(config)"
/>
</div>
<p v-else class="my-2 text-base text-gray-600">
No keys added. Click on Add Key to add one.
</p>
<Button class="mt-4" @click="showAddConfigKeyDialog = true"
>Add Key</Button
>
</div>
</div>
<div
class="hidden h-fit max-w-full flex-1 overflow-x-scroll whitespace-pre-line rounded bg-gray-100 p-4 font-mono text-base md:block"
>
<div v-if="configName" class="mb-4">{{ configName }}</div>
<div v-html="configPreview"></div>
</div>
<Dialog
:options="{
title: 'Add Config Key',
actions: [
{
label: 'Add Key',
variant: 'solid',
onClick: addConfig
}
]
}"
v-model="showAddConfigKeyDialog"
>
<template v-slot:body-content>
<div class="space-y-4">
<div>
<span class="mb-1 block text-xs text-gray-600">Key</span>
<Autocomplete
placeholder="Key"
:options="getStandardConfigKeys"
v-model="chosenStandardConfig"
@update:modelValue="handleAutocompleteSelection"
/>
</div>
<FormControl
v-if="showCustomKeyInput"
v-model="newConfig.key"
label="Custom Key"
class="w-full"
@change="isDirty = true"
/>
<FormControl
label="Type"
v-model="newConfig.type"
type="select"
:disabled="chosenStandardConfig && !showCustomKeyInput"
:options="[
'String',
'Number',
'JSON',
'Boolean',
chosenStandardConfig?.value !== 'custom_key' ? 'Password' : null
]"
@change="isDirty = true"
/>
<FormControl
v-bind="configInputProps()"
v-model="newConfig.value"
label="Value"
class="w-full"
@change="isDirty = true"
/>
</div>
</template>
</Dialog>
</div>
</Card>
</template>
<script>
import { Autocomplete } from 'jingrow-ui';
export default {
name: 'ConfigEditor',
components: {
Autocomplete
},
props: [
'title',
'subtitle',
'configName',
'configData',
'updateConfigMethod'
],
data() {
return {
isDirty: false,
showCustomKeyInput: false,
showAddConfigKeyDialog: false,
chosenStandardConfig: {
title: '',
key: ''
},
newConfig: {
key: '',
value: '',
type: 'String'
}
};
},
resources: {
configData() {
return this.configData();
},
updateConfig() {
function isValidJSON(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
const updatedConfig = this.$resources.configData.data.map(d => {
const value = d.value;
if (!isNaN(value)) d.type = 'Number';
else if (isValidJSON(value)) d.type = 'JSON';
else if (d.type === 'Password') d.type = 'Password';
else d.type = 'String';
return {
key: d.key,
value,
type: d.type
};
});
return {
...this.updateConfigMethod(updatedConfig),
async validate() {
let keys = updatedConfig.map(d => d.key);
if (keys.length !== [...new Set(keys)].length) {
return 'Duplicate key';
}
this.$resources.validateKeys.submit({
keys: JSON.stringify(keys)
});
let invalidKeys = this.$resources.validateKeys.data;
if (invalidKeys?.length > 0) {
return `Invalid key: ${invalidKeys.join(', ')}`;
}
for (let config of updatedConfig) {
if (config.type === 'JSON') {
try {
JSON.parse(JSON.stringify(config.value));
} catch (error) {
return `Invalid JSON -- ${error}`;
}
} else if (config.type === 'Number') {
try {
Number(config.value);
} catch (error) {
return 'Invalid Number';
}
}
}
},
onSuccess() {
this.isDirty = false;
this.$resources.configData.reload();
}
};
},
standardConfigKeys: {
url: 'jcloud.api.config.standard_keys',
cache: 'standardConfigKeys',
auto: true
},
validateKeys: {
url: 'jcloud.api.config.is_valid'
}
},
computed: {
configPreview() {
let obj = {};
for (let d of this.$resources.configData.data) {
let value = d.value;
if (['Boolean', 'Number'].includes(d.type)) {
value = Number(d.value);
} else if (d.type === 'JSON') {
try {
value = JSON.parse(d.value);
} catch (error) {
value = {};
}
}
obj[d.key] = value;
}
return JSON.stringify(obj, null, '&nbsp; ');
},
getStandardConfigKeys() {
return [
{
group: 'Custom',
items: [{ label: 'Create a custom key', value: 'custom_key' }]
},
{
group: 'Standard',
items: this.$resources.standardConfigKeys.data.map(d => ({
label: d.title,
value: d.key
}))
}
];
}
},
methods: {
configInputProps() {
let type = {
String: 'text',
Password: 'text',
Number: 'number',
JSON: 'textarea',
Boolean: 'select'
}[this.newConfig.type];
return {
type,
options: this.newConfig.type === 'Boolean' ? ['1', '0'] : null
};
},
addConfig() {
this.$resources.configData.data.push({
key: this.getStandardConfigKey(this.newConfig.key),
value: this.newConfig.value,
type: this.newConfig.type
});
this.isDirty = true;
this.showAddConfigKeyDialog = false;
},
handleAutocompleteSelection() {
if (this.chosenStandardConfig?.value === 'custom_key') {
this.showCustomKeyInput = true;
} else {
this.showCustomKeyInput = false;
this.newConfig.type = this.getStandardConfigType(
this.chosenStandardConfig?.value
);
}
if (this.newConfig.type === 'Boolean') {
this.newConfig.value = 0;
} else if (this.newConfig.type === 'JSON') {
this.newConfig.value = '{}';
} else {
this.newConfig.value = '';
}
this.newConfig.key = this.chosenStandardConfig?.value || '';
},
getStandardConfigType(key) {
return (
this.$resources.standardConfigKeys.data.find(d => d.key === key)
?.type || 'String'
);
},
getStandardConfigKey(key) {
return (
this.$resources.standardConfigKeys.data.find(d => d.title === key)
?.key || key
);
},
getStandardConfigTitle(key) {
return (
this.$resources.standardConfigKeys.data.find(d => d.key === key)
?.title || key
);
},
removeConfig(config) {
const index = this.$resources.configData.data.indexOf(config);
if (index > -1) this.$resources.configData.data.splice(index, 1);
this.isDirty = true;
},
updateConfig() {
if (this.isDirty) {
this.$resources.updateConfig.submit();
} else {
this.isDirty = false;
}
}
}
};
</script>