1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-16 22:50:10 +03:00

F OpenNebula/one#6652: Add custom template logos (#3188)

Signed-off-by: Victor Hansson <vhansson@opennebula.io>
This commit is contained in:
vichansson 2024-08-02 14:41:48 +03:00 committed by GitHub
parent 0b73b5e416
commit 015c735b71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 173 additions and 40 deletions

View File

@ -130,7 +130,13 @@ const General = ({
},
optionsValidate: { abortEarly: false },
content: (props) =>
Content({ ...props, isUpdate, oneConfig, adminGroup, isVrouter }),
Content({
...props,
isUpdate,
oneConfig,
adminGroup,
isVrouter,
}),
}
}

View File

@ -16,6 +16,7 @@
import { string, boolean } from 'yup'
import Image from 'client/components/Image'
import { useGetTemplateLogosQuery } from 'client/features/OneApi/logo'
import { Field, arrayToOptions } from 'client/utils'
import {
T,
@ -23,7 +24,6 @@ import {
INPUT_TYPES,
HYPERVISORS,
DEFAULT_TEMPLATE_LOGO,
TEMPLATE_LOGOS,
} from 'client/constants'
/**
@ -77,14 +77,18 @@ export const LOGO = {
label: T.Logo,
type: INPUT_TYPES.AUTOCOMPLETE,
optionsOnly: true,
values: arrayToOptions(
[['-', DEFAULT_TEMPLATE_LOGO], ...Object.entries(TEMPLATE_LOGOS)],
{
addEmpty: false,
getText: ([name]) => name,
getValue: ([, logo]) => logo,
}
),
values: () => {
const { data: logos } = useGetTemplateLogosQuery()
return arrayToOptions(
[['-', DEFAULT_TEMPLATE_LOGO], ...Object.entries(logos || {})],
{
addEmpty: false,
getText: ([name]) => name,
getValue: ([, logo]) => logo,
}
)
},
renderValue: (value) => (
<Image
alt="logo"

View File

@ -44,9 +44,17 @@ import { T, HYPERVISORS, VmTemplateFeatures } from 'client/constants'
* @param {VmTemplateFeatures} [features] - Features
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @param {boolean} isVrouter - VRouter template
* @returns {Section[]} Fields
*/
const SECTIONS = (hypervisor, isUpdate, features, oneConfig, adminGroup) =>
const SECTIONS = (
hypervisor,
isUpdate,
features,
oneConfig,
adminGroup,
isVrouter
) =>
[
{
id: 'hypervisor',
@ -134,11 +142,19 @@ const SECTIONS = (hypervisor, isUpdate, features, oneConfig, adminGroup) =>
* @param {VmTemplateFeatures} [features] - Features
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @param {boolean} isVrouter - VRouter template
* @returns {BaseSchema} Step schema
*/
const SCHEMA = (hypervisor, isUpdate, features, oneConfig, adminGroup) =>
const SCHEMA = (
hypervisor,
isUpdate,
features,
oneConfig,
adminGroup,
isVrouter
) =>
getObjectSchemaFromFields(
SECTIONS(hypervisor, isUpdate, features)
SECTIONS(hypervisor, isUpdate, features, oneConfig, adminGroup, isVrouter)
.map(({ fields }) => fields)
.flat()
)

View File

@ -107,26 +107,6 @@ export const VCENTER_FIRMWARE_TYPES = FIRMWARE_TYPES.concat(['uefi'])
export const DEFAULT_TEMPLATE_LOGO = 'images/logos/default.png'
export const TEMPLATE_LOGOS = {
'Alpine Linux': 'images/logos/alpine.png',
ALT: 'images/logos/alt.png',
Arch: 'images/logos/arch.png',
CentOS: 'images/logos/centos.png',
Debian: 'images/logos/debian.png',
Devuan: 'images/logos/devuan.png',
Fedora: 'images/logos/fedora.png',
FreeBSD: 'images/logos/freebsd.png',
HardenedBSD: 'images/logos/hardenedbsd.png',
Knoppix: 'images/logos/knoppix.png',
Linux: 'images/logos/linux.png',
Oracle: 'images/logos/oracle.png',
RedHat: 'images/logos/redhat.png',
Suse: 'images/logos/suse.png',
Ubuntu: 'images/logos/ubuntu.png',
'Windows xp': 'images/logos/windowsxp.png',
'Windows 10': 'images/logos/windows8.png',
}
/** @enum {string} FS freeze options type */
export const FS_FREEZE_OPTIONS = {
[T.None]: 'NONE',

View File

@ -33,6 +33,29 @@ const logoApi = oneApi.injectEndpoints({
providesTags: (tags) => [{ type: 'LOGO', id: tags?.logoName }],
keepUnusedDataFor: 600,
}),
getTemplateLogos: builder.query({
/**
* @returns {object} JSON struct of logo names and paths
* @throws Fails when response isn't code 200
*/
query: () => {
const name = Actions.GET_TEMPLATE_LOGOS
const command = { name, ...Commands[name] }
return { command }
},
providesTags: (tags) => {
const logos = Object.keys(tags).reduce((acc, logo) => {
acc.push({ type: 'LOGO', id: logo })
return acc
}, [])
return logos
},
keepUnusedDataFor: 600,
}),
}),
})
@ -40,6 +63,8 @@ export const {
// Queries
useGetEncodedLogoQuery,
useLazyGetEncodedLogoQuery,
useGetTemplateLogosQuery,
useLazyGetTemplateLogosQuery,
} = logoApi
export default logoApi

View File

@ -15,6 +15,7 @@
* ------------------------------------------------------------------------- */
const {
getLogo,
getAllLogos,
validateLogo,
encodeLogo,
} = require('server/routes/api/logo/utils')
@ -83,6 +84,50 @@ const getEncodedLogo = async (res = {}, next = defaultEmptyFunction) => {
next()
}
/**
* Middleware to get and send all logos with their paths.
*
* @param {object} res - The response object.
* @param {Function} next - The next middleware function.
* @returns {void}
*/
const getAllLogosHandler = async (res = {}, next = defaultEmptyFunction) => {
try {
const logos = getAllLogos() ?? {}
if (!logos) {
res.locals.httpCode = httpResponse(notFound, 'No logos found', '')
return next()
}
const validLogos = {}
for (const [name, filePath] of Object?.entries(logos)) {
const validate = validateLogo(filePath, true)
if (validate.valid) {
validLogos[name] = validate.path
}
}
if (Object.keys(validLogos)?.length === 0) {
res.locals.httpCode = httpResponse(notFound, 'No valid logos found', '')
} else {
res.locals.httpCode = httpResponse(ok, validLogos)
}
} catch (error) {
const httpError = httpResponse(
internalServerError,
'Failed to load logos',
''
)
writeInLogger(httpError)
res.locals.httpCode = httpError
}
next()
}
module.exports = {
getEncodedLogo,
getAllLogosHandler,
}

View File

@ -15,12 +15,19 @@
* ------------------------------------------------------------------------- */
const { Actions, Commands } = require('server/routes/api/logo/routes')
const { getEncodedLogo } = require('server/routes/api/logo/functions')
const { GET_LOGO } = Actions
const {
getEncodedLogo,
getAllLogosHandler,
} = require('server/routes/api/logo/functions')
const { GET_LOGO, GET_TEMPLATE_LOGOS } = Actions
module.exports = [
{
...Commands[GET_LOGO],
action: getEncodedLogo,
},
{
...Commands[GET_TEMPLATE_LOGOS],
action: getAllLogosHandler,
},
]

View File

@ -19,10 +19,12 @@ const { httpMethod } = require('../../../utils/constants/defaults')
const { GET } = httpMethod
const basepath = '/logo'
const GET_LOGO = 'get.logo'
const GET_LOGO = 'logo.brand'
const GET_TEMPLATE_LOGOS = 'logo.templates'
const Actions = {
GET_LOGO,
GET_TEMPLATE_LOGOS,
}
module.exports = {
@ -33,5 +35,10 @@ module.exports = {
httpMethod: GET,
auth: false,
},
[GET_TEMPLATE_LOGOS]: {
path: `${basepath}/templatelogos`,
httpMethod: GET,
auth: false,
},
},
}

View File

@ -14,7 +14,7 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
const { getSunstoneViewConfig } = require('server/utils/yml')
const { existsSync } = require('fs')
const { existsSync, readdirSync } = require('fs')
const path = require('path')
const { global } = require('window-or-global')
const Jimp = require('jimp')
@ -46,16 +46,19 @@ const getLogo = () => {
* Validates the specified logo file path.
*
* @param {string} logo - The logo file name to validate.
* @param {boolean} relativePaths - Return relative paths instead of absolute
* @returns {string|boolean} Full logo path or false if invalid.
*/
const validateLogo = (logo) => {
const validateLogo = (logo, relativePaths = false) => {
const imagesDirectory = global?.paths?.SUNSTONE_IMAGES
if (!logo || !imagesDirectory) {
return { valid: false, path: null }
}
const filePath = path.join(imagesDirectory, path.normalize(logo))
const filePath = path.isAbsolute(logo)
? logo
: path.join(imagesDirectory, path.normalize(logo))
if (!filePath?.startsWith(imagesDirectory)) {
return { valid: false, path: null }
@ -65,6 +68,12 @@ const validateLogo = (logo) => {
return { valid: false, path: 'Not found' }
}
if (relativePaths) {
const relativePath = path.relative(imagesDirectory, filePath)
return { valid: true, path: `images/logos/${relativePath}` }
}
return { valid: true, path: filePath }
}
@ -103,4 +112,38 @@ const encodeFavicon = async (filePath) => {
}
}
module.exports = { getLogo, validateLogo, encodeLogo, encodeFavicon }
/**
* Retrieves all logo files from the assets directory.
*
* @returns {object} A JSON object with filename as key and full path as value.
*/
const getAllLogos = () => {
const imagesDirectory = global?.paths?.SUNSTONE_IMAGES
if (!imagesDirectory || !existsSync(imagesDirectory)) {
return null
}
const files = readdirSync(imagesDirectory)
const validFilenameRegex = /^[a-zA-Z0-9-_]+\.(jpg|jpeg|png|)$/
const logos = files.reduce((acc, file) => {
if (validFilenameRegex.test(file)) {
acc[file.replace(/\.(jpg|jpeg|png)$/, '')] = path.join(
imagesDirectory,
file
)
}
return acc
}, {})
return logos
}
module.exports = {
getLogo,
getAllLogos,
validateLogo,
encodeLogo,
encodeFavicon,
}