342 lines
8.0 KiB
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, ' ');
|
|
},
|
|
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>
|