feat(ui): add configuration tab in About dialog (#4142)
* Flatten config endpoint and improve About dialog * add config resource Signed-off-by: Deluan <deluan@navidrome.org> * fix(ui): replace `==` with `===` Signed-off-by: Deluan <deluan@navidrome.org> * feat(ui): add environment variables Signed-off-by: Deluan <deluan@navidrome.org> * feat(ui): add sensitive value redaction Signed-off-by: Deluan <deluan@navidrome.org> * feat(ui): more translations Signed-off-by: Deluan <deluan@navidrome.org> * address PR comments Signed-off-by: Deluan <deluan@navidrome.org> * feat(ui): add configuration export feature in About dialog Signed-off-by: Deluan <deluan@navidrome.org> * feat(ui): translate development flags section header Signed-off-by: Deluan <deluan@navidrome.org> * refactor Signed-off-by: Deluan <deluan@navidrome.org> * feat(api): refactor routes for keepalive and insights endpoints Signed-off-by: Deluan <deluan@navidrome.org> * lint Signed-off-by: Deluan <deluan@navidrome.org> * fix(ui): enhance string escaping in formatTomlValue function Updated the formatTomlValue function to properly escape backslashes in addition to quotes. Added new test cases to ensure correct handling of strings containing both backslashes and quotes. Signed-off-by: Deluan <deluan@navidrome.org> * feat(ui): adjust dialog size Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* TOML utility functions for configuration export
|
||||
*/
|
||||
|
||||
/**
|
||||
* Separates and sorts configuration entries into regular and dev configs
|
||||
* @param {Array} configEntries - Array of config objects with key and value
|
||||
* @returns {Object} - Object with regularConfigs and devConfigs arrays, both sorted
|
||||
*/
|
||||
export const separateAndSortConfigs = (configEntries) => {
|
||||
const regularConfigs = []
|
||||
const devConfigs = []
|
||||
|
||||
configEntries?.forEach((config) => {
|
||||
// Skip configFile as it's displayed separately
|
||||
if (config.key === 'ConfigFile') {
|
||||
return
|
||||
}
|
||||
|
||||
if (config.key.startsWith('Dev')) {
|
||||
devConfigs.push(config)
|
||||
} else {
|
||||
regularConfigs.push(config)
|
||||
}
|
||||
})
|
||||
|
||||
// Sort configurations alphabetically
|
||||
regularConfigs.sort((a, b) => a.key.localeCompare(b.key))
|
||||
devConfigs.sort((a, b) => a.key.localeCompare(b.key))
|
||||
|
||||
return { regularConfigs, devConfigs }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a value to proper TOML format
|
||||
* @param {*} value - The value to format
|
||||
* @returns {string} - The TOML-formatted value
|
||||
*/
|
||||
export const formatTomlValue = (value) => {
|
||||
if (value === null || value === undefined) {
|
||||
return '""'
|
||||
}
|
||||
|
||||
const str = String(value)
|
||||
|
||||
// Boolean values
|
||||
if (str === 'true' || str === 'false') {
|
||||
return str
|
||||
}
|
||||
|
||||
// Numbers (integers and floats)
|
||||
if (/^-?\d+$/.test(str)) {
|
||||
return str // Integer
|
||||
}
|
||||
if (/^-?\d*\.\d+$/.test(str)) {
|
||||
return str // Float
|
||||
}
|
||||
|
||||
// Duration values (like "300ms", "1s", "5m")
|
||||
if (/^\d+(\.\d+)?(ns|us|µs|ms|s|m|h)$/.test(str)) {
|
||||
return `"${str}"`
|
||||
}
|
||||
|
||||
// Arrays/JSON objects
|
||||
if (str.startsWith('[') || str.startsWith('{')) {
|
||||
try {
|
||||
JSON.parse(str)
|
||||
return `"""${str}"""`
|
||||
} catch {
|
||||
return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
|
||||
}
|
||||
}
|
||||
|
||||
// String values (escape backslashes and quotes)
|
||||
return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts nested keys to TOML sections
|
||||
* @param {Array} configs - Array of config objects with key and value
|
||||
* @returns {Object} - Object with sections and rootKeys
|
||||
*/
|
||||
export const buildTomlSections = (configs) => {
|
||||
const sections = {}
|
||||
const rootKeys = []
|
||||
|
||||
configs.forEach(({ key, value }) => {
|
||||
if (key.includes('.')) {
|
||||
const parts = key.split('.')
|
||||
const sectionName = parts[0]
|
||||
const keyName = parts.slice(1).join('.')
|
||||
|
||||
if (!sections[sectionName]) {
|
||||
sections[sectionName] = []
|
||||
}
|
||||
sections[sectionName].push({ key: keyName, value })
|
||||
} else {
|
||||
rootKeys.push({ key, value })
|
||||
}
|
||||
})
|
||||
|
||||
return { sections, rootKeys }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts configuration data to TOML format
|
||||
* @param {Object} configData - The configuration data object
|
||||
* @param {Function} translate - Translation function for internationalization
|
||||
* @returns {string} - The TOML-formatted configuration
|
||||
*/
|
||||
export const configToToml = (configData, translate = (key) => key) => {
|
||||
let tomlContent = `# Navidrome Configuration\n# Generated on ${new Date().toISOString()}\n\n`
|
||||
|
||||
const { regularConfigs, devConfigs } = separateAndSortConfigs(
|
||||
configData.config,
|
||||
)
|
||||
|
||||
// Process regular configs
|
||||
const { sections: regularSections, rootKeys: regularRootKeys } =
|
||||
buildTomlSections(regularConfigs)
|
||||
|
||||
// Add root-level keys first
|
||||
if (regularRootKeys.length > 0) {
|
||||
regularRootKeys.forEach(({ key, value }) => {
|
||||
tomlContent += `${key} = ${formatTomlValue(value)}\n`
|
||||
})
|
||||
tomlContent += '\n'
|
||||
}
|
||||
|
||||
// Add sections
|
||||
Object.keys(regularSections)
|
||||
.sort()
|
||||
.forEach((sectionName) => {
|
||||
tomlContent += `[${sectionName}]\n`
|
||||
regularSections[sectionName].forEach(({ key, value }) => {
|
||||
tomlContent += `${key} = ${formatTomlValue(value)}\n`
|
||||
})
|
||||
tomlContent += '\n'
|
||||
})
|
||||
|
||||
// Add dev configs if any
|
||||
if (devConfigs.length > 0) {
|
||||
tomlContent += `# ${translate('about.config.devFlagsHeader')}\n`
|
||||
tomlContent += `# ${translate('about.config.devFlagsComment')}\n\n`
|
||||
|
||||
const { sections: devSections, rootKeys: devRootKeys } =
|
||||
buildTomlSections(devConfigs)
|
||||
|
||||
// Add dev root-level keys
|
||||
devRootKeys.forEach(({ key, value }) => {
|
||||
tomlContent += `${key} = ${formatTomlValue(value)}\n`
|
||||
})
|
||||
if (devRootKeys.length > 0) {
|
||||
tomlContent += '\n'
|
||||
}
|
||||
|
||||
// Add dev sections
|
||||
Object.keys(devSections)
|
||||
.sort()
|
||||
.forEach((sectionName) => {
|
||||
tomlContent += `[${sectionName}]\n`
|
||||
devSections[sectionName].forEach(({ key, value }) => {
|
||||
tomlContent += `${key} = ${formatTomlValue(value)}\n`
|
||||
})
|
||||
tomlContent += '\n'
|
||||
})
|
||||
}
|
||||
|
||||
return tomlContent
|
||||
}
|
||||
Reference in New Issue
Block a user