Skip to content
Merged
26 changes: 17 additions & 9 deletions admin-ui/app/components/GluuButton/GluuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const GluuButton: React.FC<GluuButtonProps> = (props) => {
minHeight,
style,
className,
useOpacityOnHover = false,
hoverOpacity,
onClick,
type = 'button',
title,
Expand All @@ -48,6 +50,8 @@ const GluuButton: React.FC<GluuButtonProps> = (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',
Expand All @@ -62,18 +66,20 @@ const GluuButton: React.FC<GluuButtonProps> = (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,
}
}, [
Expand All @@ -92,6 +98,8 @@ const GluuButton: React.FC<GluuButtonProps> = (props) => {
fontWeight,
padding,
minHeight,
useOpacityOnHover,
hoverOpacity,
style,
])

Expand Down
2 changes: 2 additions & 0 deletions admin-ui/app/components/GluuButton/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GluuPageContentStyleParams>()(
(_, { 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',
},
}),
)
35 changes: 35 additions & 0 deletions admin-ui/app/components/GluuPageContent/GluuPageContent.tsx
Original file line number Diff line number Diff line change
@@ -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<GluuPageContentProps> = 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 (
<div className={`${classes.root} ${className ?? ''}`.trim()}>
{maxWidth ? <div className={classes.wrapper}>{children}</div> : children}
</div>
)
},
)

GluuPageContent.displayName = 'GluuPageContent'

export default GluuPageContent
2 changes: 2 additions & 0 deletions admin-ui/app/components/GluuPageContent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as GluuPageContent } from './GluuPageContent'
export type { GluuPageContentProps } from './types'
9 changes: 9 additions & 0 deletions admin-ui/app/components/GluuPageContent/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ReactNode } from 'react'

export type GluuPageContentProps = {
children: ReactNode
className?: string
withVerticalPadding?: boolean
maxWidth?: number
backgroundColor?: string
}
31 changes: 31 additions & 0 deletions admin-ui/app/components/GluuSpinner/GluuSpinner.style.ts
Original file line number Diff line number Diff line change
@@ -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<GluuSpinnerStyleParams>()((_, { 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 }
26 changes: 26 additions & 0 deletions admin-ui/app/components/GluuSpinner/GluuSpinner.tsx
Original file line number Diff line number Diff line change
@@ -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<GluuSpinnerProps>(
({ 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 <output className={classes.spinner} aria-label={ariaLabel} aria-live="polite" />
},
)

GluuSpinner.displayName = 'GluuSpinner'

export default GluuSpinner
1 change: 1 addition & 0 deletions admin-ui/app/components/GluuSpinner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as GluuSpinner } from './GluuSpinner'
1 change: 1 addition & 0 deletions admin-ui/app/components/StatusBadge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { StatusBadgeProps, StatusBadgeTheme, ServiceStatusValue } from './types'
12 changes: 12 additions & 0 deletions admin-ui/app/components/StatusBadge/types.ts
Original file line number Diff line number Diff line change
@@ -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'
}
5 changes: 5 additions & 0 deletions admin-ui/app/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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,
Expand All @@ -132,6 +135,7 @@ export {
FloatGrid,
GluuBadge,
GluuButton,
GluuSpinner,
IconWithBadge,
InputGroupAddon,
Layout,
Expand All @@ -154,6 +158,7 @@ export {
ThemeProvider,
ThemeDropdown,
GluuDropdown,
GluuPageContent,
ArrowIcon,
ChevronIcon,
UncontrolledTabs,
Expand Down
2 changes: 2 additions & 0 deletions admin-ui/app/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ui'
export * from './status'
49 changes: 49 additions & 0 deletions admin-ui/app/constants/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import customColors from '@/customColors'

export type ServiceStatusValue = 'up' | 'down' | 'unknown' | 'degraded'

export const STATUS_MAP = {
'Running': 'up',
'UP': 'up',
'up': 'up',
'DOWN': 'down',
'down': 'down',
'DEGRADED': 'degraded',
'degraded': 'degraded',
'Not present': 'unknown',
'not present': 'unknown',
'unknown': 'unknown',
} as const satisfies Record<string, ServiceStatusValue>

export const DEFAULT_STATUS: ServiceStatusValue = 'unknown'

export const STATUS_LABEL_KEYS: Record<ServiceStatusValue, string> = {
up: 'messages.status_active',
down: 'messages.status_inactive',
degraded: 'messages.status_degraded',
unknown: 'messages.status_inactive',
} as const

export const STATUS_COLORS: Record<ServiceStatusValue, string> = {
up: customColors.statusActive,
down: customColors.statusInactive,
degraded: customColors.orange,
unknown: customColors.orange,
}

export const STATUS_BADGE_COLOR: Record<ServiceStatusValue, string> = {
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
27 changes: 27 additions & 0 deletions admin-ui/app/constants/ui.ts
Original file line number Diff line number Diff line change
@@ -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%'
1 change: 0 additions & 1 deletion admin-ui/app/customColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export const customColors = {
white: '#ffffff',
black: '#000000',
whiteSmoke: '#f5f5f5',

lightBackground: '#f6f6f6',
primaryDark: '#0a2540',
lightBorder: '#efefef',
Expand Down
9 changes: 9 additions & 0 deletions admin-ui/app/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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 &#39; 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)
Expand Down
6 changes: 6 additions & 0 deletions admin-ui/app/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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": {
Expand Down
Loading
Loading