diff --git a/admin-ui/app/components/GluuButton/GluuButton.tsx b/admin-ui/app/components/GluuButton/GluuButton.tsx index a76cd5cf40..6c55db3150 100644 --- a/admin-ui/app/components/GluuButton/GluuButton.tsx +++ b/admin-ui/app/components/GluuButton/GluuButton.tsx @@ -3,6 +3,7 @@ import { fontFamily } from '@/styles/fonts' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' +import { resolveBackgroundColor } from '@/utils/buttonUtils' import type { GluuButtonProps } from './types' const SIZES = { @@ -32,6 +33,7 @@ const GluuButton: React.FC = (props) => { className, useOpacityOnHover = false, hoverOpacity, + disableHoverStyles = false, onClick, type = 'button', title, @@ -50,7 +52,7 @@ const GluuButton: React.FC = (props) => { const text = textColor ?? themeColors.fontColor const border = borderColor ?? (isDark ? 'transparent' : themeColors.borderColor) const hoverBg = isDark ? themeColors.lightBackground : themeColors.borderColor - const keepBgOnHover = useOpacityOnHover && isHovered && !isDisabled + const keepBgOnHover = !disableHoverStyles && useOpacityOnHover && isHovered && !isDisabled const opacityOnHover = hoverOpacity ?? 0.5 return { @@ -66,15 +68,15 @@ const GluuButton: React.FC = (props) => { minHeight: minHeight ?? sizeConfig.minHeight, borderRadius: borderRadius ?? '6px', border: `1px solid ${border}`, - backgroundColor: keepBgOnHover - ? bg - : outlined - ? isHovered && !isDisabled - ? `${bg}15` - : 'transparent' - : isHovered && !isDisabled - ? hoverBg - : bg, + backgroundColor: resolveBackgroundColor( + disableHoverStyles, + keepBgOnHover, + outlined, + isHovered, + isDisabled, + bg, + hoverBg, + ), color: outlined ? themeColors.fontColor : text, cursor: isDisabled ? 'not-allowed' : 'pointer', opacity: isDisabled ? 0.65 : keepBgOnHover ? opacityOnHover : 1, diff --git a/admin-ui/app/components/GluuButton/types.ts b/admin-ui/app/components/GluuButton/types.ts index 5d79fa9a30..bdb3f5aa83 100644 --- a/admin-ui/app/components/GluuButton/types.ts +++ b/admin-ui/app/components/GluuButton/types.ts @@ -23,6 +23,7 @@ export interface GluuButtonProps { className?: string useOpacityOnHover?: boolean hoverOpacity?: number + disableHoverStyles?: boolean onClick?: () => void type?: 'button' | 'submit' | 'reset' title?: string diff --git a/admin-ui/app/context/theme/config.ts b/admin-ui/app/context/theme/config.ts index 509d2afe70..21abd5451c 100644 --- a/admin-ui/app/context/theme/config.ts +++ b/admin-ui/app/context/theme/config.ts @@ -39,6 +39,34 @@ const createLightTheme = () => { checkbox: { uncheckedBorder: customColors.sidebarHoverBg, }, + errorColor: customColors.accentRed, + formFooter: { + back: { + backgroundColor: customColors.statusActive, + textColor: customColors.white, + borderColor: customColors.statusActive, + }, + apply: { + backgroundColor: customColors.primaryDark, + textColor: customColors.white, + borderColor: customColors.primaryDark, + }, + cancel: { + backgroundColor: 'transparent', + textColor: customColors.primaryDark, + borderColor: customColors.primaryDark, + }, + }, + settings: { + cardBackground: customColors.white, + customParamsBox: customColors.white, + customParamsInput: customColors.whiteSmoke, + formInputBackground: customColors.whiteSmoke, + inputBorder: customColors.borderInput, + addPropertyButton: { bg: customColors.addPropertyBgDark, text: customColors.white }, + removeButton: { bg: customColors.statusInactive, text: customColors.white }, + errorButtonText: customColors.white, + }, } } @@ -80,6 +108,34 @@ const createDarkTheme = () => { checkbox: { uncheckedBorder: customColors.cedarCardBorderDark, }, + errorColor: customColors.accentRed, + formFooter: { + back: { + backgroundColor: customColors.statusActive, + textColor: customColors.white, + borderColor: customColors.statusActive, + }, + apply: { + backgroundColor: customColors.white, + textColor: customColors.primaryDark, + borderColor: customColors.white, + }, + cancel: { + backgroundColor: 'transparent', + textColor: customColors.white, + borderColor: customColors.white, + }, + }, + settings: { + cardBackground: customColors.darkCardBg, + customParamsBox: customColors.darkCardBg, + customParamsInput: customColors.darkInputBg, + formInputBackground: customColors.darkInputBg, + inputBorder: customColors.darkBorder, + addPropertyButton: { bg: customColors.white, text: customColors.addPropertyTextDark }, + removeButton: { bg: customColors.statusInactive, text: customColors.white }, + errorButtonText: customColors.white, + }, } } diff --git a/admin-ui/app/customColors.ts b/admin-ui/app/customColors.ts index 1ec17c1b2c..d2fc7ecf8e 100644 --- a/admin-ui/app/customColors.ts +++ b/admin-ui/app/customColors.ts @@ -14,12 +14,10 @@ export const customColors = { primaryDark: '#0a2540', lightBorder: '#efefef', darkBackground: '#0b2947', - darkSidebar: '#0a2540', darkBorder: '#193f66', darkCardBg: '#091e34', darkDropdownBg: '#194169', textSecondary: '#425466', - mauDark: '#4CAF50', mauPieClientCredentials: '#64b5f6', mauPieAuthCodeAccess: '#ffb74d', mauTrendClientCredentials: '#5daafa', @@ -29,6 +27,11 @@ export const customColors = { statusActiveBg: '#d3f5e6', statusInactive: '#f13f44', statusInactiveBg: '#ffe6e7', + addPropertyBgDark: '#132E4D', + mauAccentDark: '#00a65d', + addPropertyTextDark: '#1A2F45', + customParamsBoxDark: '#1E3650', + customParamsInputDark: '#1B2F45', borderInput: '#ebebeb', darkInputBg: '#15395d', lightInputBg: '#f9fafb', @@ -40,7 +43,6 @@ export const customColors = { buttonLightBg: '#f4f6f8', darkBorderGradientBase: '#00d5e6', ribbonShadowColor: '#1a237e', - // Cedarling configuration specific (synced with Figma light & dark themes) cedarCardBgDark: '#10375e', cedarCardBorderDark: '#224f7c', cedarTextSecondaryDark: '#c9dbec', @@ -50,12 +52,6 @@ export const customColors = { cedarInfoTextLight: '#4f8196', } as const -/** - * Converts a hex color string to RGB format (e.g., "#ffffff" -> "255, 255, 255"). - * @param hex - Hex color string (with or without # prefix) - * @param fallback - Optional fallback RGB string to return if hex is invalid (default: "0, 0, 0") - * @returns RGB string in format "r, g, b" or the fallback if hex is invalid - */ export const hexToRgb = (hex: string, fallback: string = '0, 0, 0'): string => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) if (!result) { diff --git a/admin-ui/app/locales/en/translation.json b/admin-ui/app/locales/en/translation.json index 18e565ad3e..d7b5845462 100644 --- a/admin-ui/app/locales/en/translation.json +++ b/admin-ui/app/locales/en/translation.json @@ -889,7 +889,6 @@ "no_role_mappings_found": "No role mappings found", "permissions_count_one": "{{count}} permission", "permissions_count_other": "{{count}} permissions", - "permissions_count": "{{count, plural, one {{count}} permission} other {{count}} permissions}}", "no_permissions_assigned": "No permissions assigned", "out_of": "out of", "permission_label": "Permission", @@ -1094,6 +1093,7 @@ "ldap_authentication": "LDAP Authentication", "services_health": "Services Health Status", "mau_dashboard": "Monthly Active Users Dashboard", + "usage_token_analytics": "Usage & Token Analytics", "mau_trend": "MAU Trend Over Time", "token_distribution": "Token Distribution by Type", "token_trends": "Token Trends Over Time", diff --git a/admin-ui/app/locales/es/translation.json b/admin-ui/app/locales/es/translation.json index 0b777c49ec..78ff2df222 100644 --- a/admin-ui/app/locales/es/translation.json +++ b/admin-ui/app/locales/es/translation.json @@ -366,7 +366,7 @@ "minimum_length": "Longitud Mínima", "month": "Mes", "monthly_active_users": "Usuarios Activos Mensuales", - "total_mau": "MAU Total", + "total_mau": "MAU total", "total_tokens": "Tokens Totales", "cc_tokens": "Tokens de Credenciales de Cliente", "authz_code_tokens": "Tokens de Código de Autorización", @@ -887,7 +887,6 @@ "no_role_mappings_found": "No se encontraron mapeos de roles", "permissions_count_one": "{{count}} permiso", "permissions_count_other": "{{count}} permisos", - "permissions_count": "{{count, plural, one {{count}} permiso} other {{count}} permisos}}", "no_permissions_assigned": "No hay permisos asignados", "out_of": "de", "permission_label": "Permiso", @@ -1084,6 +1083,7 @@ "ldap_authentication": "Autenticación LDAP", "services_health": "Estado de salud de los servicios", "mau_dashboard": "Panel de Usuarios Activos Mensuales", + "usage_token_analytics": "Uso y Analítica de Tokens", "mau_trend": "Tendencia de MAU en el Tiempo", "token_distribution": "Distribución de Tokens por Tipo", "token_trends": "Tendencias de Tokens en el Tiempo", diff --git a/admin-ui/app/locales/fr/translation.json b/admin-ui/app/locales/fr/translation.json index 61f0680be3..5e97f812bf 100644 --- a/admin-ui/app/locales/fr/translation.json +++ b/admin-ui/app/locales/fr/translation.json @@ -422,7 +422,7 @@ "client_name": "Nom du client", "client_secret": "Secret du client", "computation_pool_size": "Taille du pool de calcul", - "config_api_url": "Config API URL", + "config_api_url": "URL de l'API de configuration", "configuration_id": "Identifiant de configuration", "connection_factory_type": "Type d'usine de connexion", "connection_timeout": "Délai de connection dépassé", @@ -496,7 +496,7 @@ "minimum_length": "Longueur minimale", "month": "Mois", "monthly_active_users": "Utilisateurs actifs mensuels", - "total_mau": "MAU Total", + "total_mau": "MAU total", "total_tokens": "Jetons Totaux", "cc_tokens": "Jetons d'Identification Client", "authz_code_tokens": "Jetons de Code d'Autorisation", @@ -821,7 +821,6 @@ "no_role_mappings_found": "Aucun mappage de rôles trouvé", "permissions_count_one": "{{count}} permission", "permissions_count_other": "{{count}} permissions", - "permissions_count": "{{count, plural, one {{count}} permission} other {{count}} permissions}}", "no_permissions_assigned": "Aucune permission attribuée", "out_of": "sur", "permission_label": "Permission", @@ -1016,6 +1015,7 @@ "ldap_authentication": "Authentification LDAP", "services_health": "État de Santé des Services", "mau_dashboard": "Tableau de Bord des Utilisateurs Actifs Mensuels", + "usage_token_analytics": "Utilisation et Analyse des Jetons", "mau_trend": "Tendance MAU dans le Temps", "token_distribution": "Distribution des Jetons par Type", "token_trends": "Tendances des Jetons dans le Temps", diff --git a/admin-ui/app/locales/pt/translation.json b/admin-ui/app/locales/pt/translation.json index ab251faef9..bc25d5e2f1 100644 --- a/admin-ui/app/locales/pt/translation.json +++ b/admin-ui/app/locales/pt/translation.json @@ -406,7 +406,7 @@ "client_name": "Nome do cliente", "client_secret": "Segredo do cliente", "computation_pool_size": "Tamanho do pool de computação", - "config_api_url": "Config API URL", + "config_api_url": "URL da API de configuração", "configuration_id": "Id de configuração", "connection_factory_type": "Tipo de conexão de fábrica", "connection_timeout": "Tempo limite de conexão", @@ -489,7 +489,7 @@ "json_web_keys": "JSON Web Keys", "jwks": "Jwks", "jwks_uri": "Jwks Uri", - "list_paging_size": "List paging size", + "list_paging_size": "Tamanho da paginação da lista", "location_type": "Tipo de localização", "log_level": "Nível de registro", "log_layout": "Layout de log", @@ -508,7 +508,7 @@ "minimum_length": "Comprimento mínimo", "month": "Mês", "monthly_active_users": "Usuários ativos mensais", - "total_mau": "MAU Total", + "total_mau": "MAU total", "total_tokens": "Tokens Totais", "cc_tokens": "Tokens de Credenciais de Cliente", "authz_code_tokens": "Tokens de Código de Autorização", @@ -1008,6 +1008,7 @@ "ldap_authentication": "Autenticação Ldap", "services_health": "Estado de Saúde dos Serviços", "mau_dashboard": "Painel de Usuários Ativos Mensais", + "usage_token_analytics": "Uso e Análise de Tokens", "mau_trend": "Tendência de MAU ao Longo do Tempo", "token_distribution": "Distribuição de Tokens por Tipo", "token_trends": "Tendências de Tokens ao Longo do Tempo", diff --git a/admin-ui/app/redux/types/index.ts b/admin-ui/app/redux/types/index.ts index 2a8a2dcf2f..d2387a3867 100644 --- a/admin-ui/app/redux/types/index.ts +++ b/admin-ui/app/redux/types/index.ts @@ -543,7 +543,8 @@ export interface SmtpPluginState { // RootState: core + optional plugin reducers (dynamically registered) export interface RootState - extends CoreAppState, Partial {} + extends CoreAppState, + Partial {} // AppDispatch, useAppDispatch, useAppSelector: import from @/redux/hooks (canonical source using typeof store.dispatch) diff --git a/admin-ui/app/routes/Apps/Gluu/GluuFormFooter.tsx b/admin-ui/app/routes/Apps/Gluu/GluuFormFooter.tsx index 3cbfdd1931..4bfa563e40 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuFormFooter.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuFormFooter.tsx @@ -1,17 +1,26 @@ -import { useMemo, useCallback, memo } from 'react' +import { useContext, useMemo, useCallback, memo } from 'react' +import { Button, Divider } from 'Components' import { useTranslation } from 'react-i18next' -import { useTheme } from '@/context/theme/themeContext' -import { THEME_DARK } from '@/context/theme/constants' +import applicationStyle from 'Routes/Apps/Gluu/styles/applicationstyle' +import { ThemeContext } from 'Context/theme/themeContext' +import { DEFAULT_THEME } from '@/context/theme/constants' +import clsx from 'clsx' import { Box } from '@mui/material' import { useAppNavigation, ROUTES } from '@/helpers/navigation' -import { GluuButton } from '@/components' -import { useStyles, BUTTON_STYLES, getButtonColors } from './styles/GluuFormFooter.style' + +interface ButtonLabelProps { + isLoading: boolean + iconClass: string + label: string + loadingIconClass?: string +} interface GluuFormFooterBaseProps { showBack?: boolean backButtonLabel?: string onBack?: () => void disableBack?: boolean + backIconClass?: string showCancel?: boolean cancelButtonLabel?: string onCancel?: () => void @@ -29,26 +38,26 @@ type GluuFormFooterProps = GluuFormFooterBaseProps & | { applyButtonType: 'button'; onApply: () => void } ) -const COMMON_BUTTON_STYLE = { - minHeight: BUTTON_STYLES.height, - padding: `${BUTTON_STYLES.paddingY}px ${BUTTON_STYLES.paddingX}px`, - borderRadius: BUTTON_STYLES.borderRadius, - fontSize: BUTTON_STYLES.fontSize, - fontWeight: BUTTON_STYLES.fontWeight, - letterSpacing: BUTTON_STYLES.letterSpacing, -} +const ButtonLabel = memo((props: ButtonLabelProps) => { + const { isLoading, iconClass, label, loadingIconClass = 'fa fa-spinner fa-spin' } = props + return ( + <> + + {label} + + ) +}) -const SHARED_BUTTON_PROPS = { - useOpacityOnHover: true, - hoverOpacity: 0.85, - style: COMMON_BUTTON_STYLE, -} +ButtonLabel.displayName = 'ButtonLabel' + +const BUTTON_STYLE = { ...applicationStyle.buttonStyle, ...applicationStyle.buttonFlexIconStyles } const GluuFormFooter = ({ showBack, backButtonLabel, onBack, disableBack = false, + backIconClass = 'fa fa-arrow-circle-left', showCancel, cancelButtonLabel, onCancel, @@ -62,28 +71,10 @@ const GluuFormFooter = ({ className = '', }: GluuFormFooterProps) => { const { t } = useTranslation() - const { state } = useTheme() - const isDark = state.theme === THEME_DARK + const theme = useContext(ThemeContext) + const selectedTheme = useMemo(() => theme?.state.theme || DEFAULT_THEME, [theme?.state.theme]) const { navigateToRoute } = useAppNavigation() - const buttonStates = useMemo(() => { - const hasAnyButton = Boolean(showBack) || Boolean(showCancel) || Boolean(showApply) - const hasThreeButtons = Boolean(showBack) && Boolean(showCancel) && Boolean(showApply) - const hasRightGroup = hasThreeButtons || (!!showCancel && !showApply) - - return { - showBack: Boolean(showBack), - showCancel: Boolean(showCancel), - showApply: Boolean(showApply), - hasAnyButton, - hasThreeButtons, - hasRightGroup, - } - }, [showBack, showCancel, showApply]) - - const { classes } = useStyles({ hasRightGroup: buttonStates.hasRightGroup }) - const buttonColors = useMemo(() => getButtonColors(isDark), [isDark]) - const handleBackClick = useCallback(() => { if (onBack) { onBack() @@ -98,6 +89,21 @@ const GluuFormFooter = ({ } }, [onCancel]) + const buttonStates = useMemo(() => { + const hasAnyButton = Boolean(showBack) || Boolean(showCancel) || Boolean(showApply) + const hasAllThreeButtons = Boolean(showBack) && Boolean(showCancel) && Boolean(showApply) + + return { + showBack: Boolean(showBack), + showCancel: Boolean(showCancel), + showApply: Boolean(showApply), + hasAnyButton, + hasAllThreeButtons, + } + }, [showBack, showCancel, showApply]) + + const buttonColor = useMemo(() => `primary-${selectedTheme}`, [selectedTheme]) + const backLabel = useMemo(() => backButtonLabel || t('actions.back'), [backButtonLabel, t]) const cancelLabel = useMemo( () => cancelButtonLabel || t('actions.cancel'), @@ -105,81 +111,106 @@ const GluuFormFooter = ({ ) const applyLabel = useMemo(() => applyButtonLabel || t('actions.apply'), [applyButtonLabel, t]) + const buttonLayout = useMemo(() => { + if (!buttonStates.hasAnyButton) { + return { back: '', cancel: '', apply: '' } + } + + const back = clsx(buttonStates.showBack && 'd-flex') + + const apply = clsx( + buttonStates.showApply && 'd-flex', + buttonStates.showApply && 'ms-auto', + buttonStates.showApply && buttonStates.hasAllThreeButtons && 'me-0', + ) + + const cancel = clsx( + buttonStates.showCancel && 'd-flex', + !buttonStates.showApply && buttonStates.showCancel && 'ms-auto', + ) + + return { back, cancel, apply } + }, [buttonStates]) + if (!buttonStates.hasAnyButton) { return null } return ( - - + <> + + {buttonStates.showBack && ( - - {backLabel} - + + + )} + + {buttonStates.showApply && ( + + {applyButtonType === 'submit' ? ( + + ) : ( + + )} + )} - {!buttonStates.hasThreeButtons && buttonStates.showApply && ( - - {applyLabel} - + + )} - - {buttonStates.hasRightGroup && ( - - {buttonStates.hasThreeButtons && buttonStates.showApply && ( - - {applyLabel} - - )} - - {buttonStates.showCancel && ( - - {cancelLabel} - - )} - - )} - + ) } diff --git a/admin-ui/app/routes/Apps/Gluu/GluuInputRow.tsx b/admin-ui/app/routes/Apps/Gluu/GluuInputRow.tsx index 45e16b74e6..25ff5bf1bd 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuInputRow.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuInputRow.tsx @@ -1,10 +1,36 @@ -import { useState } from 'react' +import React, { useState } from 'react' import { Col, FormGroup, Input } from 'Components' +import type { InputProps } from 'reactstrap' import { Visibility, VisibilityOff } from '@mui/icons-material' +import type { FormikProps } from 'formik' import GluuLabel from './GluuLabel' -import customColors from '@/customColors' +import { useTheme } from '@/context/theme/themeContext' +import getThemeColor from '@/context/theme/config' +import { DEFAULT_THEME } from '@/context/theme/constants' -function GluuInputRow({ +interface GluuInputRowProps> { + label: string + name: string + type?: InputProps['type'] + value?: string | number + formik?: FormikProps | null + required?: boolean + lsize?: number + rsize?: number + doc_category?: string + disabled?: boolean + showError?: boolean + errorMessage?: string + handleChange?: ((event: React.ChangeEvent) => void) | null + doc_entry?: string + shortcode?: React.ReactNode + onFocus?: (event: React.FocusEvent) => void + rows?: number + cols?: number + isDark?: boolean +} + +function GluuInputRow>({ label, name, type = 'text', @@ -23,10 +49,13 @@ function GluuInputRow({ onFocus, rows, cols, -}: any) { + isDark, +}: GluuInputRowProps) { const [customType, setCustomType] = useState(null) + const { state } = useTheme() + const themeColors = getThemeColor(state?.theme ?? DEFAULT_THEME) - const setVisivility = () => { + const setVisivility = (): void => { if (customType) { setCustomType(null) } else { @@ -41,15 +70,16 @@ function GluuInputRow({ doc_category={doc_category} required={required} doc_entry={doc_entry || name} + isDark={isDark} /> { + value={value != null ? String(value) : ''} + onChange={(event: React.ChangeEvent) => { if (formik) { formik.handleChange(event) } @@ -74,9 +104,10 @@ function GluuInputRow({ )} )} - {showError ?
{errorMessage}
: null} + {showError ?
{errorMessage}
: null} ) } + export default GluuInputRow diff --git a/admin-ui/app/routes/Apps/Gluu/GluuLabel.tsx b/admin-ui/app/routes/Apps/Gluu/GluuLabel.tsx index 3227b4dbc2..cc7399d972 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuLabel.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuLabel.tsx @@ -1,11 +1,12 @@ -import { useMemo, type CSSProperties } from 'react' +import React, { useMemo, type CSSProperties } from 'react' import { Label } from 'Components' import 'react-tooltip/dist/react-tooltip.css' import { Tooltip as ReactTooltip } from 'react-tooltip' import { useTranslation } from 'react-i18next' import applicationStyle from './styles/applicationstyle' import { HelpOutline } from '@mui/icons-material' -import customColors from '@/customColors' +import getThemeColor from '@/context/theme/config' +import { THEME_LIGHT, THEME_DARK } from '@/context/theme/constants' interface GluuLabelProps { label: string @@ -15,9 +16,12 @@ interface GluuLabelProps { doc_entry?: string style?: CSSProperties allowColon?: boolean + isDark?: boolean } -function GluuLabel({ +const getSize = (size: number | undefined): number => (size != null ? size : 3) + +const GluuLabel: React.FC = ({ label, required, size, @@ -25,17 +29,24 @@ function GluuLabel({ doc_entry, style, allowColon = true, -}: GluuLabelProps) { + isDark: isDarkProp, +}) => { const { t, i18n } = useTranslation() - const labelColor = useMemo(() => customColors.primaryDark, []) - - function getSize() { - if (size != null) { - return size + const { labelColor, tooltipBaseStyle } = useMemo(() => { + const isDarkTheme = isDarkProp === true + const darkTheme = getThemeColor(THEME_DARK) + const lightTheme = getThemeColor(THEME_LIGHT) + return { + labelColor: isDarkTheme ? darkTheme.fontColor : lightTheme.fontColor, + tooltipBaseStyle: isDarkTheme + ? { + backgroundColor: lightTheme.menu.background, + color: lightTheme.fontColor, + } + : undefined, } - return 3 - } + }, [isDarkProp]) const labelStyle = useMemo( () => ({ @@ -45,8 +56,17 @@ function GluuLabel({ [labelColor, style], ) + const fullTooltipStyle = useMemo( + () => ({ + zIndex: 101, + maxWidth: '45vw', + ...tooltipBaseStyle, + }), + [tooltipBaseStyle], + ) + return ( -
- ()(( + _theme: Theme, + { themeColors, isDark }, +) => { + const cardBorderStyle = getCardBorderStyle({ isDark }) + + return { + summary: { + height: 120, + width: '100%', + display: 'flex', + flexDirection: 'column', + ...cardBorderStyle, + borderRadius: BORDER_RADIUS.DEFAULT, + padding: '20px 28px', + backgroundColor: themeColors.cardBg, + boxSizing: 'border-box', + }, + summaryText: { + fontFamily, + fontWeight: fontWeights.medium, + fontSize: fontSizes.md, + lineHeight: lineHeights.tight, + color: themeColors.text, + marginBottom: 20, + }, + summaryValue: { + fontFamily, + color: themeColors.text, + fontWeight: fontWeights.semiBold, + fontSize: fontSizes['3xl'], + lineHeight: lineHeights.tight, + }, + trendCard: { + width: '100%', + ...cardBorderStyle, + borderRadius: BORDER_RADIUS.DEFAULT, + padding: '24px 28px', + backgroundColor: themeColors.cardBg, + boxSizing: 'border-box', + }, + trendTitle: { + fontFamily, + fontStyle: 'normal', + fontWeight: fontWeights.medium, + fontSize: fontSizes.xl, + lineHeight: lineHeights.tight, + color: themeColors.text, + marginTop: 0, + marginBottom: 16, + }, + } +}) diff --git a/admin-ui/plugins/admin/components/MAU/MauPage.tsx b/admin-ui/plugins/admin/components/MAU/MauPage.tsx index 2c34d6b999..34609c8586 100644 --- a/admin-ui/plugins/admin/components/MAU/MauPage.tsx +++ b/admin-ui/plugins/admin/components/MAU/MauPage.tsx @@ -1,5 +1,5 @@ -import React, { useState, useMemo, useCallback } from 'react' -import { Container, Card, CardBody, Row, Col, Alert } from 'Components' +import React, { useState, useMemo, useCallback, useEffect } from 'react' +import { Row, Col, Alert, GluuPageContent } from 'Components' import { useTranslation } from 'react-i18next' import SetTitle from 'Utils/SetTitle' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' @@ -7,27 +7,60 @@ import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import { useCedarling } from '@/cedarling' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' -import dayjs, { type Dayjs } from 'dayjs' +import { useTheme } from '@/context/theme/themeContext' +import customColors, { hexToRgb } from '@/customColors' +import { THEME_DARK, DEFAULT_THEME } from '@/context/theme/constants' +import { SummaryCard as DashboardSummaryCard } from '@/routes/Dashboards/components' +import { useMauStyles } from './MauPage.style' +import type { Dayjs } from 'dayjs' +import { createDate, subtractDate } from '@/utils/dayjsUtils' import { useMauStats } from './hooks' import { DEFAULT_DATE_RANGE_MONTHS } from './constants' import type { MauDateRange } from './types' import { DateRangeSelector, - MauSummaryCards, MauTrendChart, TokenDistributionChart, TokenTrendChart, } from './components' -import GluuText from 'Routes/Apps/Gluu/GluuText' const MauPage: React.FC = () => { const { t } = useTranslation() SetTitle(t('titles.mau_dashboard')) - - const [startDate, setStartDate] = useState( - dayjs().subtract(DEFAULT_DATE_RANGE_MONTHS, 'months'), + const { state: themeState } = useTheme() + const currentTheme = themeState?.theme || DEFAULT_THEME + const isDark = currentTheme === THEME_DARK + + const dashboardThemeColors = useMemo(() => { + const baseColors = isDark + ? { + cardBg: customColors.darkCardBg, + cardBorder: `rgba(${hexToRgb(customColors.darkBorderGradientBase)}, 0.2)`, + text: customColors.white, + textSecondary: customColors.textMutedDark, + } + : { + cardBg: customColors.white, + cardBorder: customColors.lightBorder, + text: customColors.primaryDark, + textSecondary: customColors.textSecondary, + } + + return { + ...baseColors, + statusCardBg: baseColors.cardBg, + statusCardBorder: baseColors.cardBorder, + } + }, [isDark]) + const { classes: mauClasses } = useMauStyles({ + themeColors: { cardBg: dashboardThemeColors.cardBg, text: dashboardThemeColors.text }, + isDark, + }) + + const [startDate, setStartDate] = useState(() => + subtractDate(createDate(), DEFAULT_DATE_RANGE_MONTHS, 'months'), ) - const [endDate, setEndDate] = useState(dayjs()) + const [endDate, setEndDate] = useState(() => createDate()) const [selectedPreset, setSelectedPreset] = useState(DEFAULT_DATE_RANGE_MONTHS) const { hasCedarReadPermission, authorizeHelper } = useCedarling() @@ -39,7 +72,7 @@ const MauPage: React.FC = () => { [hasCedarReadPermission, mauResourceId], ) - React.useEffect(() => { + useEffect(() => { authorizeHelper(mauScopes) }, [authorizeHelper, mauScopes]) @@ -71,8 +104,8 @@ const MauPage: React.FC = () => { }, []) const handlePresetSelect = useCallback((months: number) => { - setStartDate(dayjs().subtract(months, 'months')) - setEndDate(dayjs()) + setStartDate(subtractDate(createDate(), months, 'months')) + setEndDate(createDate()) setSelectedPreset(months) }, []) @@ -82,32 +115,32 @@ const MauPage: React.FC = () => { const hasData = mauData.length > 0 + const summaryCards = useMemo( + () => [ + { text: t('fields.total_mau'), value: summary.totalMau }, + { text: t('fields.total_tokens'), value: summary.totalTokens }, + { text: t('fields.cc_tokens'), value: summary.clientCredentialsTokens }, + { text: t('fields.authz_code_tokens'), value: summary.authCodeTokens }, + ], + [summary, t], + ) + return ( - - - - - - - {t('titles.mau_dashboard')} - - - - - - - + +
+ +
{isError && ( @@ -125,7 +158,21 @@ const MauPage: React.FC = () => { {hasData && ( <> - + + + + {summaryCards.map((card) => ( + + + + ))} + + + @@ -139,7 +186,7 @@ const MauPage: React.FC = () => { )} -
+
) diff --git a/admin-ui/plugins/admin/components/MAU/components/DateRangeSelector.tsx b/admin-ui/plugins/admin/components/MAU/components/DateRangeSelector.tsx index 5a7dc1b5a4..efa0b609d7 100644 --- a/admin-ui/plugins/admin/components/MAU/components/DateRangeSelector.tsx +++ b/admin-ui/plugins/admin/components/MAU/components/DateRangeSelector.tsx @@ -1,13 +1,29 @@ -import React from 'react' -import { Button, ButtonGroup } from 'Components' +import React, { useMemo } from 'react' +import Box from '@mui/material/Box' import Grid from '@mui/material/Grid' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { DatePicker } from '@mui/x-date-pickers/DatePicker' import { useTranslation } from 'react-i18next' -import { useTheme } from 'Context/theme/themeContext' +import { useTheme } from '@/context/theme/themeContext' +import getThemeColor from '@/context/theme/config' +import { THEME_DARK } from '@/context/theme/constants' +import customColors from '@/customColors' +import { GluuButton } from '@/components/GluuButton' +import GluuText from 'Routes/Apps/Gluu/GluuText' +import { fontFamily, fontWeights, fontSizes, lineHeights, letterSpacing } from '@/styles/fonts' import type { DateRangeSelectorProps } from '../types' import { DATE_PRESETS } from '../constants' +import { getDatePickerTextFieldSlotProps } from '../utils' + +const VIEW_BUTTON_STYLE = { + minWidth: 96, + borderRadius: 8, + fontFamily, + fontStyle: 'normal' as const, + lineHeight: lineHeights.normal, + letterSpacing: letterSpacing.button, +} const DateRangeSelector: React.FC = ({ startDate, @@ -22,55 +38,104 @@ const DateRangeSelector: React.FC = ({ const { t } = useTranslation() const { state } = useTheme() const selectedTheme = state.theme + const isDark = selectedTheme === THEME_DARK + const themeColors = getThemeColor(selectedTheme) + const datePickerTextFieldSlotProps = useMemo( + () => getDatePickerTextFieldSlotProps(isDark), + [isDark], + ) + + const presetButtonBg = (isSelected: boolean) => + isSelected + ? themeColors.inputBackground + : (themeColors.dashboard.supportCard ?? themeColors.menu.background) + const presetButtonBorder = themeColors.borderColor return ( - + - - {DATE_PRESETS.map((preset) => { - const isSelected = selectedPreset === preset.months - return ( - - ) - })} - + + {t('titles.usage_token_analytics')} + - - - - - - - - + + + + {DATE_PRESETS.map((preset, index) => { + const isSelected = selectedPreset === preset.months + const isFirst = index === 0 + const isLast = index === DATE_PRESETS.length - 1 + return ( + onPresetSelect(preset.months)} + theme={selectedTheme} + outlined={!isSelected} + textColor={themeColors.fontColor} + backgroundColor={presetButtonBg(isSelected)} + borderColor={presetButtonBorder} + disableHoverStyles + style={{ + minWidth: 110, + borderRadius: isFirst ? '8px 0 0 8px' : isLast ? '0 8px 8px 0' : 0, + marginLeft: isFirst ? 0 : -1, + }} + > + {t(preset.labelKey)} + + ) + })} + - - - - + + + + + + + + + + + + + + + {t('actions.view')} + + + ) diff --git a/admin-ui/plugins/admin/components/MAU/components/MauSummaryCards.tsx b/admin-ui/plugins/admin/components/MAU/components/MauSummaryCards.tsx index ca0cb4908d..a415ab8e8c 100644 --- a/admin-ui/plugins/admin/components/MAU/components/MauSummaryCards.tsx +++ b/admin-ui/plugins/admin/components/MAU/components/MauSummaryCards.tsx @@ -1,7 +1,9 @@ import React, { useMemo } from 'react' import { Card, CardBody, Row, Col } from 'Components' import { useTranslation } from 'react-i18next' -import { useTheme } from 'Context/theme/themeContext' +import { useTheme } from '@/context/theme/themeContext' +import GluuText from 'Routes/Apps/Gluu/GluuText' +import customColors from '@/customColors' import type { MauSummary } from '../types' import { getChartColors } from '../constants' import { formatNumber, formatPercentChange } from '../utils' @@ -22,19 +24,29 @@ const SummaryCard: React.FC = ({ title, value, change, color } const showChange = change !== undefined const isPositive = change !== undefined && change > 0 const isNegative = change !== undefined && change < 0 - const changeColor = isPositive ? '#28a745' : isNegative ? '#dc3545' : '#6c757d' + const changeColor = isPositive + ? customColors.statusActive + : isNegative + ? customColors.statusInactive + : customColors.textSecondary const changeIcon = isPositive ? 'fa-arrow-up' : isNegative ? 'fa-arrow-down' : 'fa-minus' return ( -
{title}
-

{formatNumber(value)}

+ + {title} + + + {formatNumber(value)} + {showChange && (
- + {formatPercentChange(change)} - {t('messages.vs_previous_period')} + + {t('messages.vs_previous_period')} +
)}
@@ -58,17 +70,17 @@ const MauSummaryCards: React.FC = ({ summary }) => { title: t('fields.total_tokens'), value: summary.totalTokens, change: summary.tokenChange, - color: '#6c757d', + color: chartColors.totalTokens, }, { title: t('fields.cc_tokens'), value: summary.clientCredentialsTokens, - color: chartColors.clientCredentials, + color: chartColors.pieClientCredentials, }, { title: t('fields.authz_code_tokens'), value: summary.authCodeTokens, - color: chartColors.authCodeAccess, + color: chartColors.pieAuthCodeAccess, }, ] diff --git a/admin-ui/plugins/admin/components/MAU/components/MauTrendChart.tsx b/admin-ui/plugins/admin/components/MAU/components/MauTrendChart.tsx index ddc4667be9..ea8a34ce11 100644 --- a/admin-ui/plugins/admin/components/MAU/components/MauTrendChart.tsx +++ b/admin-ui/plugins/admin/components/MAU/components/MauTrendChart.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react' -import { Card, CardBody, CardHeader } from 'Components' +import { Card, CardBody } from 'Components' import { XAxis, YAxis, @@ -9,15 +9,31 @@ import { CartesianGrid, ResponsiveContainer, } from 'recharts' +import type { TooltipProps } from 'recharts' import { useTranslation } from 'react-i18next' -import { useTheme } from 'Context/theme/themeContext' +import { useTheme } from '@/context/theme/themeContext' +import getThemeColor from '@/context/theme/config' +import { THEME_DARK } from '@/context/theme/constants' +import GluuText from 'Routes/Apps/Gluu/GluuText' +import { useMauStyles } from '../MauPage.style' import type { MauChartProps } from '../types' +import TooltipDesign from '@/routes/Dashboards/Chart/TooltipDesign' +import type { TooltipPayloadItem } from '@/routes/Dashboards/types' import { getChartColors } from '../constants' import { formatMonth, formatNumber } from '../utils' const MauTrendChart: React.FC = ({ data }) => { const { t } = useTranslation() const { state } = useTheme() + const themeColors = getThemeColor(state.theme) + const isDark = state.theme === THEME_DARK + const { classes } = useMauStyles({ + themeColors: { + cardBg: themeColors.dashboard.supportCard ?? themeColors.menu.background, + text: themeColors.fontColor, + }, + isDark, + }) const chartColors = useMemo(() => getChartColors(state.theme), [state.theme]) const chartData = data.map((entry) => ({ @@ -26,17 +42,32 @@ const MauTrendChart: React.FC = ({ data }) => { })) return ( - - {t('titles.mau_trend')} + + + {t('titles.mau_trend')} + - - - + + + [formatNumber(value), t('fields.monthly_active_users')]} - labelFormatter={(label) => label} + content={(props: TooltipProps) => ( + + formatNumber(typeof value === 'number' && !Number.isNaN(value) ? value : 0) + } + /> + )} /> = ({ summary }) => { const { t } = useTranslation() const { state } = useTheme() + const themeColors = getThemeColor(state.theme) + const isDark = state.theme === THEME_DARK + const { classes } = useMauStyles({ + themeColors: { + cardBg: themeColors.dashboard.supportCard ?? themeColors.menu.background, + text: themeColors.fontColor, + }, + isDark, + }) const chartColors = useMemo(() => getChartColors(state.theme), [state.theme]) const data = useMemo( @@ -35,9 +50,11 @@ const TokenDistributionChart: React.FC = ({ summary const hasData = summary.totalTokens > 0 return ( - - {t('titles.token_distribution')} + + + {t('titles.token_distribution')} + {hasData ? ( @@ -49,19 +66,49 @@ const TokenDistributionChart: React.FC = ({ summary outerRadius={90} paddingAngle={2} dataKey="value" - label={({ percent }) => `${(percent * 100).toFixed(0)}%`} + label={({ cx, cy, midAngle, innerRadius, outerRadius, percent }) => { + const RADIAN = Math.PI / 180 + const radius = innerRadius + (outerRadius - innerRadius) * 0.5 + const x = cx + radius * Math.cos(-midAngle * RADIAN) + const y = cy + radius * Math.sin(-midAngle * RADIAN) + return ( + + {`${(percent * 100).toFixed(0)}%`} + + ) + }} > {data.map((entry, index) => ( ))} - formatNumber(value)} /> - + ) => ( + + )} + /> + ) : (
- {t('messages.no_mau_data')} + + {t('messages.no_mau_data')} +
)}
diff --git a/admin-ui/plugins/admin/components/MAU/components/TokenTrendChart.tsx b/admin-ui/plugins/admin/components/MAU/components/TokenTrendChart.tsx index b6550fb6b3..96b02aa382 100644 --- a/admin-ui/plugins/admin/components/MAU/components/TokenTrendChart.tsx +++ b/admin-ui/plugins/admin/components/MAU/components/TokenTrendChart.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react' -import { Card, CardBody, CardHeader } from 'Components' +import { Card, CardBody } from 'Components' import { XAxis, YAxis, @@ -10,15 +10,31 @@ import { ResponsiveContainer, Legend, } from 'recharts' +import type { TooltipProps } from 'recharts' import { useTranslation } from 'react-i18next' -import { useTheme } from 'Context/theme/themeContext' +import { useTheme } from '@/context/theme/themeContext' +import getThemeColor from '@/context/theme/config' +import { THEME_DARK } from '@/context/theme/constants' +import GluuText from 'Routes/Apps/Gluu/GluuText' +import { useMauStyles } from '../MauPage.style' import type { MauChartProps } from '../types' +import TooltipDesign from '@/routes/Dashboards/Chart/TooltipDesign' +import type { TooltipPayloadItem } from '@/routes/Dashboards/types' import { getChartColors } from '../constants' import { formatMonth, formatNumber } from '../utils' const TokenTrendChart: React.FC = ({ data }) => { const { t } = useTranslation() const { state } = useTheme() + const themeColors = getThemeColor(state.theme) + const isDark = state.theme === THEME_DARK + const { classes } = useMauStyles({ + themeColors: { + cardBg: themeColors.dashboard.supportCard ?? themeColors.menu.background, + text: themeColors.fontColor, + }, + isDark, + }) const chartColors = useMemo(() => getChartColors(state.theme), [state.theme]) const chartData = data.map((entry) => ({ @@ -29,16 +45,31 @@ const TokenTrendChart: React.FC = ({ data }) => { })) return ( - - {t('titles.token_trends')} + + + {t('titles.token_trends')} + - - - - formatNumber(value)} /> - + + + + ) => ( + + )} + /> + { const transformed = transformApiResponse(data) return augmentMauData(transformed, dateRange.startDate, dateRange.endDate) diff --git a/admin-ui/plugins/admin/components/MAU/utils/datePicker.ts b/admin-ui/plugins/admin/components/MAU/utils/datePicker.ts new file mode 100644 index 0000000000..2853660993 --- /dev/null +++ b/admin-ui/plugins/admin/components/MAU/utils/datePicker.ts @@ -0,0 +1,28 @@ +import customColors from '@/customColors' + +export const getDatePickerTextFieldSlotProps = (isDark: boolean) => ({ + size: 'small' as const, + InputLabelProps: { shrink: true }, + sx: { + '& .MuiInputBase-root': { + borderRadius: 1.5, + backgroundColor: isDark ? customColors.darkInputBg : customColors.lightInputBg, + color: isDark ? customColors.white : undefined, + }, + '& .MuiInputBase-input': { + color: isDark ? customColors.white : undefined, + }, + '& .MuiInputLabel-root': { + 'color': isDark ? customColors.white : undefined, + '&.Mui-focused': { + color: isDark ? customColors.white : undefined, + }, + }, + '& .MuiIconButton-root': { + 'color': isDark ? customColors.white : undefined, + '& .MuiSvgIcon-root': { + color: isDark ? customColors.white : undefined, + }, + }, + }, +}) diff --git a/admin-ui/plugins/admin/components/MAU/utils/index.ts b/admin-ui/plugins/admin/components/MAU/utils/index.ts index 1d4b26bba3..d2d6515c53 100644 --- a/admin-ui/plugins/admin/components/MAU/utils/index.ts +++ b/admin-ui/plugins/admin/components/MAU/utils/index.ts @@ -6,6 +6,8 @@ export { augmentMauData, } from './dataAugmentation' +export { getDatePickerTextFieldSlotProps } from './datePicker' + export { formatMonth, formatNumber, diff --git a/admin-ui/plugins/admin/components/Settings/SettingsPage.style.ts b/admin-ui/plugins/admin/components/Settings/SettingsPage.style.ts new file mode 100644 index 0000000000..b524f4dd53 --- /dev/null +++ b/admin-ui/plugins/admin/components/Settings/SettingsPage.style.ts @@ -0,0 +1,207 @@ +import { makeStyles } from 'tss-react/mui' +import type { Theme } from '@mui/material/styles' +import { SPACING, BORDER_RADIUS } from '@/constants' +import { fontFamily, fontWeights, fontSizes, lineHeights, letterSpacing } from '@/styles/fonts' +import { getCardBorderStyle } from '@/styles/cardBorderStyles' +import { themeConfig } from '@/context/theme/config' +import customColors from '@/customColors' + +type ThemeColors = (typeof themeConfig)[keyof typeof themeConfig] + +interface SettingsStylesParams { + isDark: boolean + themeColors: ThemeColors +} + +export const useStyles = makeStyles()(( + theme: Theme, + { isDark, themeColors }, +) => { + const cardBorderStyle = getCardBorderStyle({ isDark }) + const settings = themeColors.settings + + // Custom params: box and inputs same background as form inputs above (per Figma) + const customParamsInputBorder = isDark ? customColors.darkBorder : customColors.borderInput + const customParamsBoxBorder = isDark ? customColors.darkBorder : customColors.borderInput + + return { + settingsCard: { + backgroundColor: settings.cardBackground, + ...cardBorderStyle, + borderRadius: BORDER_RADIUS.DEFAULT, + width: '100%', + minHeight: 480, + position: 'relative', + overflow: 'visible', + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + }, + header: { + paddingTop: `${SPACING.CONTENT_PADDING}px`, + paddingLeft: `${SPACING.CONTENT_PADDING}px`, + paddingRight: `${SPACING.CONTENT_PADDING}px`, + paddingBottom: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + position: 'relative', + minHeight: 84, + }, + headerTitle: { + fontFamily: fontFamily, + fontWeight: fontWeights.medium, + fontSize: fontSizes.lg, + lineHeight: lineHeights.tight, + color: themeColors.fontColor, + margin: 0, + }, + headerSubtitle: { + fontFamily: fontFamily, + fontWeight: fontWeights.medium, + fontSize: fontSizes.sm, + lineHeight: lineHeights.relaxed, + color: themeColors.textMuted, + margin: 0, + }, + headerDivider: { + position: 'absolute', + left: 0, + bottom: 0, + width: '100%', + borderBottom: `1px solid ${themeColors.borderColor}`, + zIndex: 0, + }, + settingsLabels: { + '& label, & label h5, & label .MuiSvgIcon-root': { + color: `${themeColors.fontColor} !important`, + fontFamily: fontFamily, + fontSize: fontSizes.base, + fontStyle: 'normal', + fontWeight: fontWeights.semiBold, + lineHeight: 'normal', + letterSpacing: letterSpacing.normal, + }, + }, + content: { + paddingTop: `${SPACING.SECTION_GAP}px`, + paddingLeft: `${SPACING.CONTENT_PADDING}px`, + paddingRight: `${SPACING.CONTENT_PADDING}px`, + paddingBottom: `${SPACING.CONTENT_PADDING}px`, + width: '100%', + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'column', + gap: SPACING.SECTION_GAP, + [theme.breakpoints.down('sm')]: { + paddingLeft: `${SPACING.PAGE}px`, + paddingRight: `${SPACING.PAGE}px`, + }, + }, + formSection: { + display: 'flex', + flexDirection: 'column', + gap: SPACING.CARD_GAP, + }, + fieldsGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', + columnGap: SPACING.CARD_GAP, + rowGap: SPACING.CARD_GAP, + width: '100%', + [theme.breakpoints.down('md')]: { + gridTemplateColumns: '1fr', + }, + }, + fieldItem: { + width: '100%', + }, + fieldItemFullWidth: { + width: '100%', + gridColumn: '1 / -1', + }, + formWithInputs: { + '& input, & select': { + backgroundColor: settings.formInputBackground, + border: `1px solid ${settings.inputBorder}`, + borderRadius: 6, + color: themeColors.fontColor, + padding: '8px 12px', + }, + '& input:disabled': { + backgroundColor: `${settings.formInputBackground} !important`, + border: `1px solid ${settings.inputBorder} !important`, + color: `${themeColors.fontColor} !important`, + opacity: 1, + cursor: 'not-allowed', + }, + '& input::placeholder': { + color: themeColors.textMuted, + }, + }, + customParamsBox: { + backgroundColor: settings.formInputBackground, + borderRadius: BORDER_RADIUS.DEFAULT, + border: `1px solid ${customParamsBoxBorder}`, + padding: `12px ${SPACING.CARD_PADDING}px ${SPACING.CARD_PADDING}px`, + width: '100%', + boxSizing: 'border-box', + }, + customParamsHeader: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + alignContent: 'center', + marginBottom: 16, + gap: 12, + }, + customParamsTitle: { + fontFamily: fontFamily, + fontWeight: 600, + fontSize: '15px', + fontStyle: 'normal', + lineHeight: 1.4, + letterSpacing: letterSpacing.normal, + color: themeColors.fontColor, + margin: 0, + padding: 0, + }, + customParamsBody: { + display: 'flex', + flexDirection: 'column', + gap: SPACING.CARD_CONTENT_GAP, + }, + customParamsRow: { + display: 'flex', + gap: SPACING.CARD_CONTENT_GAP, + alignItems: 'stretch', + flexWrap: 'wrap', + }, + customParamsInput: { + 'flex': '1 1 200px', + 'minWidth': 120, + 'minHeight': 44, + 'boxSizing': 'border-box', + 'backgroundColor': `${settings.cardBackground} !important`, + 'border': `1px solid ${customParamsInputBorder} !important`, + 'borderRadius': 6, + 'padding': '10px 12px', + 'color': themeColors.fontColor, + '&::placeholder': { + color: themeColors.textMuted, + }, + '&:focus, &:active': { + backgroundColor: `${settings.cardBackground} !important`, + color: themeColors.fontColor, + border: `1px solid ${customParamsInputBorder} !important`, + outline: 'none', + boxShadow: 'none', + }, + }, + customParamsError: { + color: themeColors.errorColor, + fontSize: fontSizes.sm, + marginTop: 4, + }, + } +}) diff --git a/admin-ui/plugins/admin/components/Settings/SettingsPage.tsx b/admin-ui/plugins/admin/components/Settings/SettingsPage.tsx index e832d7d7ae..453fd618db 100644 --- a/admin-ui/plugins/admin/components/Settings/SettingsPage.tsx +++ b/admin-ui/plugins/admin/components/Settings/SettingsPage.tsx @@ -1,35 +1,19 @@ import React, { useMemo, useCallback, useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useFormik } from 'formik' -import { useDispatch, useSelector } from 'react-redux' -import { - Card, - CardBody, - FormGroup, - Col, - Label, - Badge, - InputGroup, - CustomInput, - Form, - Alert, - Button, - Input, - Accordion, - AccordionHeader, - AccordionBody, -} from 'Components' +import { useAppSelector, useAppDispatch } from '@/redux/hooks' +import { FormGroup, InputGroup, CustomInput, Form, Alert, Input, GluuPageContent } from 'Components' import GluuLabel from 'Routes/Apps/Gluu/GluuLabel' import GluuToogleRow from 'Routes/Apps/Gluu/GluuToogleRow' import GluuInputRow from 'Routes/Apps/Gluu/GluuInputRow' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' -import GluuFormFooter from 'Routes/Apps/Gluu/GluuFormFooter' +import GluuThemeFormFooter from 'Routes/Apps/Gluu/GluuThemeFormFooter' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' +import GluuText from 'Routes/Apps/Gluu/GluuText' import { SETTINGS } from 'Utils/ApiResources' import SetTitle from 'Utils/SetTitle' -import applicationStyle from 'Routes/Apps/Gluu/styles/applicationstyle' -import { useTheme } from 'Context/theme/themeContext' -import getThemeColor from 'Context/theme/config' +import { useTheme } from '@/context/theme/themeContext' +import getThemeColor from '@/context/theme/config' import { SIMPLE_PASSWORD_AUTH } from 'Plugins/auth-server/common/Constants' import { CedarlingLogType, @@ -55,32 +39,25 @@ import { logAudit } from '@/utils/AuditLogger' import { UPDATE } from '@/audit/UserActionType' import { ADMIN_UI_SETTINGS } from 'Plugins/admin/redux/audit/Resources' import { getErrorMessage } from 'Plugins/schema/utils/errorHandler' -import type { RootState } from '@/redux/sagas/types/audit' -import { DEFAULT_THEME } from '@/context/theme/constants' -import customColors from '@/customColors' +import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' +import { GluuButton } from '@/components/GluuButton' +import { useStyles } from './SettingsPage.style' const PAGING_SIZE_OPTIONS = [1, 5, 10, 20] as const const DEFAULT_PAGING_SIZE = PAGING_SIZE_OPTIONS[2] const SCRIPTS_FETCH_LIMIT = 200 -const FORM_GROUP_ROW_STYLE = { justifyContent: 'space-between' } -const LABEL_CONTAINER_STYLE: React.CSSProperties = { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - paddingRight: '15px', -} - const SettingsPage: React.FC = () => { const { t } = useTranslation() - const dispatch = useDispatch() + const dispatch = useAppDispatch() const { state: themeState } = useTheme() + const isDark = (themeState?.theme ?? DEFAULT_THEME) === THEME_DARK const queryClient = useQueryClient() const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() - const userinfo = useSelector((state: RootState) => state.authReducer?.userinfo) - const clientId = useSelector((state: RootState) => state.authReducer?.config?.clientId) + const userinfo = useAppSelector((state) => state.authReducer?.userinfo) + const clientId = useAppSelector((state) => state.authReducer?.config?.clientId) const { data: config, @@ -130,15 +107,9 @@ const SettingsPage: React.FC = () => { ? stored : DEFAULT_PAGING_SIZE }, []) - const selectedTheme = useMemo(() => themeState.theme || DEFAULT_THEME, [themeState.theme]) + const selectedTheme = useMemo(() => themeState?.theme || DEFAULT_THEME, [themeState?.theme]) const themeColors = useMemo(() => getThemeColor(selectedTheme), [selectedTheme]) - const badgeStyle = useMemo( - () => ({ - backgroundColor: themeColors.background, - color: customColors.white, - }), - [themeColors.background], - ) + const { classes } = useStyles({ isDark, themeColors }) const configApiUrl = useMemo(() => { if (typeof window === 'undefined') return 'N/A' const windowWithConfig = window as Window & { configApiBaseUrl?: string } @@ -310,9 +281,14 @@ const SettingsPage: React.FC = () => {
{msg}
))} - + ) @@ -321,235 +297,251 @@ const SettingsPage: React.FC = () => { return ( - - - {renderErrorAlert()} -
- - - - - - - - - - - - - - ) => - handlePagingSizeChange(Number.parseInt(e.target.value, 10)) - } + +
+
+ {renderErrorAlert()} + +
+
+ +
+ +
+ +
+ +
+ + + + ) => + handlePagingSizeChange(Number.parseInt(e.target.value, 10)) + } + disabled={!canWriteSettings} + > + {PAGING_SIZE_OPTIONS.map((option) => ( + + ))} + + + +
+ +
+ + label="fields.sessionTimeoutInMins" + name="sessionTimeoutInMins" + type="number" + formik={formik} + lsize={12} + rsize={12} + value={formik.values.sessionTimeoutInMins} + doc_category={SETTINGS} + doc_entry="sessionTimeoutInMins" + errorMessage={formik.errors.sessionTimeoutInMins} + showError={Boolean( + formik.errors.sessionTimeoutInMins && formik.touched.sessionTimeoutInMins, + )} disabled={!canWriteSettings} - > - {PAGING_SIZE_OPTIONS.map((option) => ( - - ))} - - - - - - - - - - - - +
+ +
+ + + + + + {authScripts.map((item) => ( + + ))} + + + +
+ +
+ + + + isLabelVisible={false} + label="fields.showCedarLogs?" + name="cedarlingLogType" + formik={formik} + value={formik.values.cedarlingLogType === CedarlingLogType.STD_OUT} + doc_category={SETTINGS} + doc_entry="cedarSwitch" + lsize={12} + rsize={12} + disabled={!canWriteSettings} + isDark={isDark} + handler={(event: React.ChangeEvent) => { + formik.setFieldValue( + 'cedarlingLogType', + event.target.checked ? CedarlingLogType.STD_OUT : CedarlingLogType.OFF, + ) + }} + /> + +
+
+ +
+
+ + + {t('fields.custom_params_auth')} + + + { + const currentParams = formik.values.additionalParameters || [] + formik.setFieldValue('additionalParameters', [ + ...currentParams, + { id: crypto.randomUUID(), key: '', value: '' }, + ]) + }} > - - {authScripts.map((item) => ( - - ))} - - - - - - - - - ) => { - formik.setFieldValue( - 'cedarlingLogType', - event.target.checked ? CedarlingLogType.STD_OUT : CedarlingLogType.OFF, - ) - }} - /> - - - - - -
- {t('fields.custom_params_auth')} -
-
- - - - - {(formik.values.additionalParameters || []).map( - (param: { key?: string; value?: string }, index: number) => ( - - - - - - - - - - - - ), - )} - - + + {t('actions.add_property')} +
+
+
+ {(formik.values.additionalParameters || []).map( + (param: { id: string; key?: string; value?: string }, index: number) => ( +
+ + + { + const currentParams = formik.values.additionalParameters || [] + const newParams = currentParams.filter((p) => p.id !== param.id) + formik.setFieldValue('additionalParameters', newParams) + }} + > + + {t('actions.remove')} + +
+ ), + )} +
{showAdditionalParametersError && additionalParametersErrorText && additionalParametersErrorText.trim() && ( -
+
{additionalParametersErrorText}
)} - - - - - - - +
+ + + +
+
+ ) diff --git a/admin-ui/plugins/admin/helper/settings.ts b/admin-ui/plugins/admin/helper/settings.ts index 3f18f1fb3e..60ad0cfade 100644 --- a/admin-ui/plugins/admin/helper/settings.ts +++ b/admin-ui/plugins/admin/helper/settings.ts @@ -6,11 +6,15 @@ export type SettingsConfigData = Pick< 'sessionTimeoutInMins' | 'acrValues' | 'cedarlingLogType' | 'additionalParameters' > +export interface AdditionalParameterFormItem extends KeyValuePair { + id: string +} + export interface SettingsFormValues { sessionTimeoutInMins: number acrValues: string cedarlingLogType: CedarlingLogType - additionalParameters: KeyValuePair[] + additionalParameters: AdditionalParameterFormItem[] } export const sanitizeAdditionalParameters = (params?: KeyValuePair[] | null): KeyValuePair[] => { @@ -36,6 +40,7 @@ export const buildSettingsInitialValues = ( cedarlingLogType: (configData?.cedarlingLogType as CedarlingLogType) ?? CedarlingLogType.OFF, additionalParameters: sanitizeAdditionalParameters(configData?.additionalParameters).map( (param) => ({ + id: crypto.randomUUID(), key: param.key || '', value: param.value || '', }),