diff --git a/admin-ui/app/components/GluuButton/GluuButton.tsx b/admin-ui/app/components/GluuButton/GluuButton.tsx index a44830d387..a76cd5cf40 100644 --- a/admin-ui/app/components/GluuButton/GluuButton.tsx +++ b/admin-ui/app/components/GluuButton/GluuButton.tsx @@ -30,6 +30,8 @@ const GluuButton: React.FC = (props) => { minHeight, style, className, + useOpacityOnHover = false, + hoverOpacity, onClick, type = 'button', title, @@ -48,6 +50,8 @@ 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 opacityOnHover = hoverOpacity ?? 0.5 return { display: 'inline-flex', @@ -62,18 +66,20 @@ const GluuButton: React.FC = (props) => { minHeight: minHeight ?? sizeConfig.minHeight, borderRadius: borderRadius ?? '6px', border: `1px solid ${border}`, - backgroundColor: outlined - ? isHovered && !isDisabled - ? `${bg}15` - : 'transparent' - : isHovered && !isDisabled - ? hoverBg - : bg, + backgroundColor: keepBgOnHover + ? bg + : outlined + ? isHovered && !isDisabled + ? `${bg}15` + : 'transparent' + : isHovered && !isDisabled + ? hoverBg + : bg, color: outlined ? themeColors.fontColor : text, cursor: isDisabled ? 'not-allowed' : 'pointer', - opacity: isDisabled ? 0.65 : 1, + opacity: isDisabled ? 0.65 : keepBgOnHover ? opacityOnHover : 1, width: block ? '100%' : 'auto', - transition: 'background-color 0.15s ease-in-out', + transition: 'background-color 0.15s ease-in-out, opacity 0.15s ease-in-out', ...style, } }, [ @@ -92,6 +98,8 @@ const GluuButton: React.FC = (props) => { fontWeight, padding, minHeight, + useOpacityOnHover, + hoverOpacity, style, ]) diff --git a/admin-ui/app/components/GluuButton/types.ts b/admin-ui/app/components/GluuButton/types.ts index bd56e533ec..5d79fa9a30 100644 --- a/admin-ui/app/components/GluuButton/types.ts +++ b/admin-ui/app/components/GluuButton/types.ts @@ -21,6 +21,8 @@ export interface GluuButtonProps { minHeight?: string | number style?: CSSProperties className?: string + useOpacityOnHover?: boolean + hoverOpacity?: number onClick?: () => void type?: 'button' | 'submit' | 'reset' title?: string diff --git a/admin-ui/app/components/GluuPageContent/GluuPageContent.style.ts b/admin-ui/app/components/GluuPageContent/GluuPageContent.style.ts new file mode 100644 index 0000000000..8d390b0f00 --- /dev/null +++ b/admin-ui/app/components/GluuPageContent/GluuPageContent.style.ts @@ -0,0 +1,27 @@ +import { makeStyles } from 'tss-react/mui' +import { SPACING } from '@/constants' + +interface GluuPageContentStyleParams { + withVerticalPadding: boolean + maxWidth?: number + background: string +} + +export const useStyles = makeStyles()( + (_, { withVerticalPadding, maxWidth, background }) => ({ + root: { + maxWidth: '100vw', + width: '100%', + padding: withVerticalPadding ? `${SPACING.PAGE}px` : `0 ${SPACING.PAGE}px`, + boxSizing: 'border-box', + backgroundColor: background, + }, + wrapper: { + width: '100%', + maxWidth: maxWidth ?? '100%', + display: 'flex', + justifyContent: 'center', + flexDirection: 'column', + }, + }), +) diff --git a/admin-ui/app/components/GluuPageContent/GluuPageContent.tsx b/admin-ui/app/components/GluuPageContent/GluuPageContent.tsx new file mode 100644 index 0000000000..7807757ced --- /dev/null +++ b/admin-ui/app/components/GluuPageContent/GluuPageContent.tsx @@ -0,0 +1,35 @@ +import React, { memo, useContext, useMemo } from 'react' +import { ThemeContext } from '@/context/theme/themeContext' +import getThemeColor from '@/context/theme/config' +import { DEFAULT_THEME } from '@/context/theme/constants' +import customColors from '@/customColors' +import { useStyles } from './GluuPageContent.style' +import type { GluuPageContentProps } from './types' + +const GluuPageContent: React.FC = memo( + ({ children, className, withVerticalPadding = true, maxWidth, backgroundColor }) => { + const themeContext = useContext(ThemeContext) + const currentTheme = useMemo( + () => themeContext?.state.theme || DEFAULT_THEME, + [themeContext?.state.theme], + ) + const themeColors = useMemo(() => getThemeColor(currentTheme), [currentTheme]) + const background = backgroundColor ?? themeColors?.background ?? customColors.lightBackground + + const { classes } = useStyles({ + withVerticalPadding, + maxWidth, + background, + }) + + return ( +
+ {maxWidth ?
{children}
: children} +
+ ) + }, +) + +GluuPageContent.displayName = 'GluuPageContent' + +export default GluuPageContent diff --git a/admin-ui/app/components/GluuPageContent/index.ts b/admin-ui/app/components/GluuPageContent/index.ts new file mode 100644 index 0000000000..cfd7918687 --- /dev/null +++ b/admin-ui/app/components/GluuPageContent/index.ts @@ -0,0 +1,2 @@ +export { default as GluuPageContent } from './GluuPageContent' +export type { GluuPageContentProps } from './types' diff --git a/admin-ui/app/components/GluuPageContent/types.ts b/admin-ui/app/components/GluuPageContent/types.ts new file mode 100644 index 0000000000..ae534acc79 --- /dev/null +++ b/admin-ui/app/components/GluuPageContent/types.ts @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' + +export type GluuPageContentProps = { + children: ReactNode + className?: string + withVerticalPadding?: boolean + maxWidth?: number + backgroundColor?: string +} diff --git a/admin-ui/app/components/GluuSpinner/GluuSpinner.style.ts b/admin-ui/app/components/GluuSpinner/GluuSpinner.style.ts new file mode 100644 index 0000000000..d438d62cbc --- /dev/null +++ b/admin-ui/app/components/GluuSpinner/GluuSpinner.style.ts @@ -0,0 +1,31 @@ +import { makeStyles } from 'tss-react/mui' +import customColors, { hexToRgb } from '@/customColors' + +const TRACK_COLOR_DARK = `rgba(${hexToRgb(customColors.white)}, 0.12)` +const TRACK_COLOR_LIGHT = `rgba(${hexToRgb(customColors.black)}, 0.08)` + +interface GluuSpinnerStyleParams { + size: number + isDark: boolean +} + +const useStyles = makeStyles()((_, { size, isDark }) => ({ + '@keyframes spin': { + '0%': { transform: 'rotate(0deg)' }, + '100%': { transform: 'rotate(360deg)' }, + }, + 'spinner': { + display: 'block', + width: size, + height: size, + borderRadius: '50%', + border: `${Math.max(3, Math.floor(size / 12))}px solid ${ + isDark ? TRACK_COLOR_DARK : TRACK_COLOR_LIGHT + }`, + borderTopColor: customColors.logo, + animation: 'spin 0.8s linear infinite', + flexShrink: 0, + }, +})) + +export { useStyles } diff --git a/admin-ui/app/components/GluuSpinner/GluuSpinner.tsx b/admin-ui/app/components/GluuSpinner/GluuSpinner.tsx new file mode 100644 index 0000000000..531322a796 --- /dev/null +++ b/admin-ui/app/components/GluuSpinner/GluuSpinner.tsx @@ -0,0 +1,26 @@ +import React, { useContext } from 'react' +import { ThemeContext } from '@/context/theme/themeContext' +import { THEME_DARK, DEFAULT_THEME } from '@/context/theme/constants' +import { useStyles } from './GluuSpinner.style' + +interface GluuSpinnerProps { + 'size'?: number + 'isDark'?: boolean + 'aria-label'?: string +} + +const GluuSpinner = React.memo( + ({ size = 48, 'isDark': isDarkProp, 'aria-label': ariaLabel = 'Loading' }) => { + const themeContext = useContext(ThemeContext) + const currentTheme = themeContext?.state.theme || DEFAULT_THEME + const isDark = isDarkProp ?? currentTheme === THEME_DARK + + const { classes } = useStyles({ size, isDark }) + + return + }, +) + +GluuSpinner.displayName = 'GluuSpinner' + +export default GluuSpinner diff --git a/admin-ui/app/components/GluuSpinner/index.ts b/admin-ui/app/components/GluuSpinner/index.ts new file mode 100644 index 0000000000..ee90b9f00f --- /dev/null +++ b/admin-ui/app/components/GluuSpinner/index.ts @@ -0,0 +1 @@ +export { default as GluuSpinner } from './GluuSpinner' diff --git a/admin-ui/app/components/StatusBadge/index.ts b/admin-ui/app/components/StatusBadge/index.ts new file mode 100644 index 0000000000..cf65cfbe0d --- /dev/null +++ b/admin-ui/app/components/StatusBadge/index.ts @@ -0,0 +1 @@ +export type { StatusBadgeProps, StatusBadgeTheme, ServiceStatusValue } from './types' diff --git a/admin-ui/app/components/StatusBadge/types.ts b/admin-ui/app/components/StatusBadge/types.ts new file mode 100644 index 0000000000..7dcb96a5b4 --- /dev/null +++ b/admin-ui/app/components/StatusBadge/types.ts @@ -0,0 +1,12 @@ +export type StatusBadgeTheme = 'active' | 'inactive' | 'success' | 'danger' | 'warning' | 'info' + +export type ServiceStatusValue = 'up' | 'down' | 'unknown' | 'degraded' + +export interface StatusBadgeProps { + text?: string + theme?: StatusBadgeTheme + isDark?: boolean + isActive?: boolean + status?: ServiceStatusValue + variant?: 'default' | 'health' +} diff --git a/admin-ui/app/components/index.tsx b/admin-ui/app/components/index.tsx index 290506e768..1f7c830079 100755 --- a/admin-ui/app/components/index.tsx +++ b/admin-ui/app/components/index.tsx @@ -11,6 +11,7 @@ import EmptyLayout from './EmptyLayout' import ExtendedDropdown from './ExtendedDropdown' import FloatGrid from './FloatGrid' import { GluuBadge } from './GluuBadge' +import { GluuSpinner } from './GluuSpinner' import { GluuButton } from './GluuButton' import IconWithBadge from './IconWithBadge' import InputGroupAddon from './InputGroupAddon' @@ -30,6 +31,7 @@ import SidebarTrigger from './SidebarTrigger' import { ThemeClass, ThemeProvider, ThemeConsumer } from './Theme' import { ThemeDropdown } from './ThemeDropdown' import { GluuDropdown } from './GluuDropdown' +import { GluuPageContent } from './GluuPageContent' import { ArrowIcon, ChevronIcon } from './SVG' import UncontrolledTabs from './UncontrolledTabs' import Wizard from './Wizard' @@ -116,6 +118,7 @@ export type { export type { DropdownPosition as ThemeDropdownPosition } from './GluuDropdown/types' export type { GluuBadgeProps, BadgeSize, BadgeTheme } from './GluuBadge/types' export type { GluuButtonProps, ButtonSize, ButtonTheme } from './GluuButton/types' +export type { GluuPageContentProps } from './GluuPageContent' export { Accordion, AccordionHeader, @@ -132,6 +135,7 @@ export { FloatGrid, GluuBadge, GluuButton, + GluuSpinner, IconWithBadge, InputGroupAddon, Layout, @@ -154,6 +158,7 @@ export { ThemeProvider, ThemeDropdown, GluuDropdown, + GluuPageContent, ArrowIcon, ChevronIcon, UncontrolledTabs, diff --git a/admin-ui/app/constants/index.ts b/admin-ui/app/constants/index.ts new file mode 100644 index 0000000000..010119fcba --- /dev/null +++ b/admin-ui/app/constants/index.ts @@ -0,0 +1,2 @@ +export * from './ui' +export * from './status' diff --git a/admin-ui/app/constants/status.ts b/admin-ui/app/constants/status.ts new file mode 100644 index 0000000000..39c1225bbe --- /dev/null +++ b/admin-ui/app/constants/status.ts @@ -0,0 +1,45 @@ +import customColors from '@/customColors' + +export type ServiceStatusValue = 'up' | 'down' | 'unknown' | 'degraded' + +export const STATUS_MAP = { + 'running': 'up', + 'up': 'up', + 'down': 'down', + 'degraded': 'degraded', + 'not present': 'unknown', + 'unknown': 'unknown', +} as const satisfies Record + +export const DEFAULT_STATUS: ServiceStatusValue = 'unknown' + +export const STATUS_LABEL_KEYS: Record = { + up: 'messages.status_active', + down: 'messages.status_inactive', + degraded: 'messages.status_degraded', + unknown: 'messages.status_inactive', +} as const + +export const STATUS_COLORS: Record = { + up: customColors.statusActive, + down: customColors.statusInactive, + degraded: customColors.orange, + unknown: customColors.orange, +} as const + +export const STATUS_BADGE_COLOR: Record = { + up: 'success', + down: 'danger', + degraded: 'warning', + unknown: 'warning', +} as const + +export const STATUS_DETAILS = [ + { label: 'menus.oauthserver', key: 'jans-auth' }, + { label: 'dashboard.config_api', key: 'jans-config-api' }, + { label: 'menus.fido', key: 'jans-fido2' }, + { label: 'dashboard.casa', key: 'jans-casa' }, + { label: 'dashboard.key_cloak', key: 'keycloak' }, + { label: 'menus.scim', key: 'jans-scim' }, + { label: 'dashboard.jans_lock', key: 'jans-lock' }, +] as const diff --git a/admin-ui/app/constants/ui.ts b/admin-ui/app/constants/ui.ts new file mode 100644 index 0000000000..31e361e782 --- /dev/null +++ b/admin-ui/app/constants/ui.ts @@ -0,0 +1,27 @@ +export const SPACING = { + PAGE: 24, + CONTENT_PADDING: 40, + SECTION_GAP: 24, + CARD_GAP: 24, + CARD_PADDING: 24, + CARD_CONTENT_GAP: 8, +} as const + +export const BORDER_RADIUS = { + DEFAULT: 16, + LARGE: 24, + MEDIUM: 14, + SMALL: 5, + CIRCLE: '50%', + THIN: '1.5px', +} as const + +export const GRADIENT_POSITION = { + TOP_RIGHT: 'top right', + TOP_LEFT: 'top left', + BOTTOM_RIGHT: 'bottom right', + BOTTOM_LEFT: 'bottom left', + CENTER: 'center', +} as const + +export const ELLIPSE_SIZE = '200% 160%' diff --git a/admin-ui/app/customColors.ts b/admin-ui/app/customColors.ts index 8332dab250..647a409a4b 100644 --- a/admin-ui/app/customColors.ts +++ b/admin-ui/app/customColors.ts @@ -10,7 +10,6 @@ export const customColors = { white: '#ffffff', black: '#000000', whiteSmoke: '#f5f5f5', - lightBackground: '#f6f6f6', primaryDark: '#0a2540', lightBorder: '#efefef', diff --git a/admin-ui/app/i18n.ts b/admin-ui/app/i18n.ts index 587dfa26da..67c57053ee 100644 --- a/admin-ui/app/i18n.ts +++ b/admin-ui/app/i18n.ts @@ -7,6 +7,8 @@ import translationFr from './locales/fr/translation.json' import translationPt from './locales/pt/translation.json' import translationEs from './locales/es/translation.json' +const isDev = process.env.NODE_ENV === 'development' + const i18nConfig: InitOptions = { resources: { en: { @@ -34,6 +36,13 @@ const i18nConfig: InitOptions = { escapeValue: false, // React already escapes HTML for XSS protection, so we don't need i18next to escape // This prevents double-escaping which causes ' to appear instead of ' }, + + parseMissingKeyHandler: isDev + ? (key: string, defaultValue?: string) => { + console.warn(`[i18n] Missing translation key: "${key}"`) + return defaultValue ?? key + } + : undefined, } i18n.use(initReactI18next).init(i18nConfig) diff --git a/admin-ui/app/locales/en/translation.json b/admin-ui/app/locales/en/translation.json index e62ea6d214..029ca64504 100644 --- a/admin-ui/app/locales/en/translation.json +++ b/admin-ui/app/locales/en/translation.json @@ -94,6 +94,7 @@ "config_api_status": "Config API Status", "key_cloak": "Keycloak", "jans_lock": "Jans Lock", + "casa": "CASA", "jans_link": "Jans Link", "access_denied": "Access Denied", "access_denied_message": "You do not have permission to access this page", @@ -918,6 +919,8 @@ "no_services_found": "No services found.", "status_online": "Online", "status_offline": "Offline", + "status_active": "Active", + "status_inactive": "Inactive", "status_degraded": "Degraded", "status_unknown": "Unknown", "no_mau_data": "No data available for the selected period.", @@ -1034,6 +1037,9 @@ "expires_before": "Expiration Before Date", "charMoreThan512": "characters over limit (maximum 512)", "charLessThan10": "characters required (minimum 10)", + "charRequiredMin": "{{count}} characters required (minimum {{min}})", + "charRequiredMin_one": "{{count}} character required (minimum {{min}})", + "charRequiredMin_other": "{{count}} characters required (minimum {{min}})", "more": " more" }, "titles": { diff --git a/admin-ui/app/locales/es/translation.json b/admin-ui/app/locales/es/translation.json index 108d9c85db..b51f2c32c6 100644 --- a/admin-ui/app/locales/es/translation.json +++ b/admin-ui/app/locales/es/translation.json @@ -94,6 +94,7 @@ "config_api_status": "Estado de la API de Configuración", "key_cloak": "Keycloak", "jans_lock": "Jans Lock", + "casa": "CASA", "jans_link": "Jans Link", "access_denied": "Acceso Denegado", "access_denied_message": "No tienes permiso para acceder a esta página", @@ -914,6 +915,8 @@ "no_services_found": "No se encontraron servicios.", "status_online": "En línea", "status_offline": "Fuera de línea", + "status_active": "Activo", + "status_inactive": "Inactivo", "status_degraded": "Degradado", "status_unknown": "Desconocido", "no_mau_data": "No hay datos disponibles para el período seleccionado.", @@ -1024,6 +1027,9 @@ "expires_before": "Expira antes de la fecha", "charMoreThan512": "caracteres excedidos (máximo 512)", "charLessThan10": "se requieren más caracteres (mínimo 10)", + "charRequiredMin": "{{count}} caracteres requeridos (mínimo {{min}})", + "charRequiredMin_one": "{{count}} carácter requerido (mínimo {{min}})", + "charRequiredMin_other": "{{count}} caracteres requeridos (mínimo {{min}})", "more": " más" }, "titles": { diff --git a/admin-ui/app/locales/fr/translation.json b/admin-ui/app/locales/fr/translation.json index 171e1e9587..750cefadde 100644 --- a/admin-ui/app/locales/fr/translation.json +++ b/admin-ui/app/locales/fr/translation.json @@ -43,6 +43,7 @@ "config_api_status": "État de l'API de configuration", "key_cloak": "Keycloak", "jans_lock": "Jans Lock", + "casa": "CASA", "jans_link": "Lien Jans", "access_denied": "Accès refusé", "access_denied_message": "Vous n'êtes pas autorisé à accéder à cette page", @@ -54,6 +55,7 @@ "authentication": "Authentification", "adminui": "Administratrice", "assets": "Assets", + "scim": "SCIM", "config-api": "Config-API", "security": "Sécurité", "webhooks": "Webhooks", @@ -106,6 +108,7 @@ "propriétés": "Propriétés du serveur d'authentification", "reports": "Rapports", "roles": "Les rôles", + "fido": "FIDO", "schema": "Schéma", "user_claims": "Réclamations des utilisateurs", "scopes": "Portées", @@ -848,6 +851,8 @@ "no_services_found": "Aucun service trouvé.", "status_online": "En ligne", "status_offline": "Hors ligne", + "status_active": "Actif", + "status_inactive": "Inactif", "status_degraded": "Dégradé", "status_unknown": "Inconnu", "no_mau_data": "Aucune donnée disponible pour la période sélectionnée.", @@ -958,6 +963,9 @@ "expires_before": "Expiration avant la date", "charMoreThan512": "caractères acima do limite (maximum 512)", "charLessThan10": "caractères nécessaires (minimum 10)", + "charRequiredMin": "{{count}} caractères requis (minimum {{min}})", + "charRequiredMin_one": "{{count}} caractère requis (minimum {{min}})", + "charRequiredMin_other": "{{count}} caractères requis (minimum {{min}})", "more": " plus" }, "titles": { diff --git a/admin-ui/app/locales/pt/translation.json b/admin-ui/app/locales/pt/translation.json index 9651a70a42..7ec65eeb8a 100644 --- a/admin-ui/app/locales/pt/translation.json +++ b/admin-ui/app/locales/pt/translation.json @@ -43,6 +43,7 @@ "config_api_status": "Status da API de configuração", "key_cloak": "Keycloak", "jans_lock": "Jans Lock", + "casa": "CASA", "jans_link": "Link Jans", "access_denied": "Acesso negado", "access_denied_message": "Entre em contato com o administrador para obter ajuda", @@ -54,6 +55,7 @@ "authentication": "Autenticação", "adminui": "Admin", "assets": "Assets", + "scim": "SCIM", "config-api": "Config-API", "security": "Segurança", "saml": "SAML", @@ -105,6 +107,7 @@ "propriedades": "Propriedades do servidor de autenticação", "reports": "Relatórios", "roles": "Funções", + "fido": "FIDO", "schema": "Esquema", "user_claims": "Reivindicações do usuário", "scopes": "Escopos", @@ -843,6 +846,8 @@ "no_services_found": "Nenhum serviço encontrado.", "status_online": "Online", "status_offline": "Offline", + "status_active": "Ativo", + "status_inactive": "Inativo", "status_degraded": "Degradado", "status_unknown": "Desconhecido", "no_mau_data": "Nenhum dado disponível para o período selecionado.", @@ -951,6 +956,9 @@ "expires_before": "Expira antes da data", "charMoreThan512": "caracteres acima do limite (máximo 512)", "charLessThan10": "caracteres necessários (mínimo 10)", + "charRequiredMin": "{{count}} caracteres necessários (mínimo {{min}})", + "charRequiredMin_one": "{{count}} caractere necessário (mínimo {{min}})", + "charRequiredMin_other": "{{count}} caracteres necessários (mínimo {{min}})", "more": " mais" }, "titles": { diff --git a/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx b/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx index d1f2fac3b0..cc9392f074 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx @@ -1,65 +1,45 @@ -import React, { useState, useEffect, useContext, useRef, useMemo } from 'react' -import { - FormGroup, - Col, - Input, - Button, - Modal, - Badge, - ModalHeader, - ModalBody, - ModalFooter, - Collapse, -} from 'reactstrap' +import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react' +import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' -import { ThemeContext } from 'Context/theme/themeContext' -import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' -import getThemeColor from '@/context/theme/config' +import { useTheme } from 'Context/theme/themeContext' +import { THEME_DARK } from '@/context/theme/constants' import PropTypes from 'prop-types' -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp' +import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined' import { useSelector } from 'react-redux' import { useWebhookDialogAction } from 'Utils/hooks' import { useCedarling } from '@/cedarling' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' -import customColors from '@/customColors' import type { RootState } from '@/redux/sagas/types/audit' -import type { GluuCommitDialogOperation, GluuCommitDialogProps, JsonValue } from './types/index' -import { Alert, Box } from '@mui/material' +import type { GluuCommitDialogProps } from './types/index' +import customColors from '@/customColors' +import { useStyles } from './styles/GluuCommitDialog.style' +import { + getCommitMessageValidationError, + COMMIT_MESSAGE_MIN_LENGTH, + COMMIT_MESSAGE_MAX_LENGTH, +} from '@/utils/validation/commitMessage' +import GluuText from './GluuText' import { GluuButton } from '@/components' const USER_MESSAGE = 'user_action_message' -const isJsonValueArray = (value: JsonValue): value is JsonValue[] => { - return Array.isArray(value) -} - const GluuCommitDialog = ({ handler, modal, onAccept, formik, - operations, label, placeholderLabel, - inputType, feature, isLicenseLabel = false, - alertMessage, - alertSeverity, }: GluuCommitDialogProps) => { const { t } = useTranslation() const { hasCedarReadPermission, authorizeHelper } = useCedarling() - const theme = useContext(ThemeContext) - const selectedTheme = theme?.state.theme || DEFAULT_THEME - const isDark = selectedTheme === THEME_DARK - const inverseTheme = isDark ? 'light' : 'dark' - const inverseColors = getThemeColor(inverseTheme) - const themeColors = getThemeColor(selectedTheme) - const [active, setActive] = useState(false) - const [isOpen, setIsOpen] = useState(null) + const { state: themeState } = useTheme() + const isDark = themeState.theme === THEME_DARK + const { classes } = useStyles({ isDark }) const [userMessage, setUserMessage] = useState('') const { loadingWebhooks, webhookModal } = useSelector((state: RootState) => state.webhookReducer) @@ -86,264 +66,171 @@ const GluuCommitDialog = ({ const prevModalRef = useRef(false) useEffect(() => { - setActive(userMessage.length >= 10 && userMessage.length <= 512) - if (modal && !prevModalRef.current) { setUserMessage('') } prevModalRef.current = modal - }, [userMessage, modal]) + }, [modal]) + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + setUserMessage(e.target.value) + }, []) + + const messageLength = userMessage.length + const isValid = + messageLength >= COMMIT_MESSAGE_MIN_LENGTH && messageLength <= COMMIT_MESSAGE_MAX_LENGTH + const errorMessageText = useMemo( + () => getCommitMessageValidationError(messageLength, t), + [messageLength, t], + ) - function handleAccept() { - if (formik) { - formik.setFieldValue('action_message', userMessage) - } - onAccept(userMessage) - } - const closeModal = () => { + const titleText = useMemo(() => { + if (isLicenseLabel) return t('messages.licenseAuditLog') + if (!label || label === '') return t('messages.action_commit_question_title') + return label + }, [isLicenseLabel, label, t]) + + const placeholderText = useMemo( + () => placeholderLabel || t('placeholders.action_commit_message'), + [placeholderLabel, t], + ) + + const closeModal = useCallback(() => { handler() onCloseModal() setUserMessage('') - } + }, [handler, onCloseModal]) - const renderBadges = (values: JsonValue[]) => { - return ( -
- {values.map((data) => ( - - {JSON.stringify(data)} - - ))} -
- ) - } - - const renderArrayValue = (values: JsonValue[], key: number) => { - if (!values.length) { - return ( - - "" - - ) + const handleAccept = useCallback(() => { + if (formik) { + formik.setFieldValue('action_message', userMessage) } + onAccept(userMessage) + closeModal() + }, [formik, onAccept, userMessage, closeModal]) + + const handleOverlayKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ' || e.key === 'Escape') { + e.preventDefault() + closeModal() + } + }, + [closeModal], + ) - return ( -
- {isOpen === key ? ( - {renderBadges(values)} - ) : ( - renderBadges(values.slice(0, 2)) - )} - {values.length > 2 && ( - - )} -
- ) - } + const handleModalKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault() + closeModal() + } + e.stopPropagation() + }, + [closeModal], + ) if (!modal) { return <> } - return ( - <> - {(webhookModal || loadingWebhooks) && canReadWebhooks ? ( - <>{webhookTriggerModal({ closeModal })} - ) : ( - - - {}} - style={{ color: customColors.logo }} - className="fa fa-2x fa-info fa-fw modal-icon mb-3" - role="img" - aria-hidden="true" - > - - {isLicenseLabel - ? t('messages.licenseAuditLog') - : !label || label === '' - ? t('messages.action_commit_question') - : label} - - - -
{webhookTriggerModal({ closeModal })} + ) : ( + <> +