From 015c735b71eb7029f732d902b268d17831a737b8 Mon Sep 17 00:00:00 2001 From: vichansson Date: Fri, 2 Aug 2024 14:41:48 +0300 Subject: [PATCH] F OpenNebula/one#6652: Add custom template logos (#3188) Signed-off-by: Victor Hansson --- .../CreateForm/Steps/General/index.js | 8 ++- .../Steps/General/informationSchema.js | 22 ++++---- .../CreateForm/Steps/General/schema.js | 22 ++++++-- .../src/client/constants/vmTemplate.js | 20 -------- .../src/client/features/OneApi/logo.js | 25 +++++++++ .../src/server/routes/api/logo/functions.js | 45 ++++++++++++++++ .../src/server/routes/api/logo/index.js | 11 +++- .../src/server/routes/api/logo/routes.js | 9 +++- .../src/server/routes/api/logo/utils.js | 51 +++++++++++++++++-- 9 files changed, 173 insertions(+), 40 deletions(-) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/index.js index 62ec9cb0eaa..7ac850a53e8 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/index.js @@ -130,7 +130,13 @@ const General = ({ }, optionsValidate: { abortEarly: false }, content: (props) => - Content({ ...props, isUpdate, oneConfig, adminGroup, isVrouter }), + Content({ + ...props, + isUpdate, + oneConfig, + adminGroup, + isVrouter, + }), } } diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema.js index afc022c1c46..b542b933bcd 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema.js @@ -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) => ( logo +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() ) diff --git a/src/fireedge/src/client/constants/vmTemplate.js b/src/fireedge/src/client/constants/vmTemplate.js index 870bad6c316..4285167ca85 100644 --- a/src/fireedge/src/client/constants/vmTemplate.js +++ b/src/fireedge/src/client/constants/vmTemplate.js @@ -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', diff --git a/src/fireedge/src/client/features/OneApi/logo.js b/src/fireedge/src/client/features/OneApi/logo.js index 17248cd33d9..f660adcda7d 100644 --- a/src/fireedge/src/client/features/OneApi/logo.js +++ b/src/fireedge/src/client/features/OneApi/logo.js @@ -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 diff --git a/src/fireedge/src/server/routes/api/logo/functions.js b/src/fireedge/src/server/routes/api/logo/functions.js index 375137bc494..42d6262d020 100644 --- a/src/fireedge/src/server/routes/api/logo/functions.js +++ b/src/fireedge/src/server/routes/api/logo/functions.js @@ -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, } diff --git a/src/fireedge/src/server/routes/api/logo/index.js b/src/fireedge/src/server/routes/api/logo/index.js index 572307d3896..a92385f6eab 100644 --- a/src/fireedge/src/server/routes/api/logo/index.js +++ b/src/fireedge/src/server/routes/api/logo/index.js @@ -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, + }, ] diff --git a/src/fireedge/src/server/routes/api/logo/routes.js b/src/fireedge/src/server/routes/api/logo/routes.js index 89694e11a9b..f2dfcf509e0 100644 --- a/src/fireedge/src/server/routes/api/logo/routes.js +++ b/src/fireedge/src/server/routes/api/logo/routes.js @@ -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, + }, }, } diff --git a/src/fireedge/src/server/routes/api/logo/utils.js b/src/fireedge/src/server/routes/api/logo/utils.js index 702c17db5f3..fca5b8741e1 100644 --- a/src/fireedge/src/server/routes/api/logo/utils.js +++ b/src/fireedge/src/server/routes/api/logo/utils.js @@ -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, +}