diff --git a/webui/package.json b/webui/package.json
index b75ac6d1b..4d6e64f3c 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -101,5 +101,5 @@
"public"
]
},
- "packageManager": "yarn@4.9.1"
+ "packageManager": "yarn@4.12.0"
}
diff --git a/webui/src/components/CopyableText.tsx b/webui/src/components/CopyableText.tsx
new file mode 100644
index 000000000..42c8f1bf2
--- /dev/null
+++ b/webui/src/components/CopyableText.tsx
@@ -0,0 +1,39 @@
+import { CSS, Text } from '@traefiklabs/faency'
+import { useContext } from 'react'
+
+import CopyButton from 'components/buttons/CopyButton'
+import { ToastContext } from 'contexts/toasts'
+
+type CopyableTextProps = {
+ notifyText?: string
+ text: string
+ css?: CSS
+}
+
+export default function CopyableText({ notifyText, text, css }: CopyableTextProps) {
+ const { addToast } = useContext(ToastContext)
+
+ return (
+
+ {text}
+ {
+ if (notifyText) addToast({ message: notifyText, severity: 'success' })
+ }}
+ css={{ display: 'inline-block', height: 20, verticalAlign: 'middle', ml: '$1' }}
+ iconOnly
+ />
+
+ )
+}
diff --git a/webui/src/components/ScrollableCard.tsx b/webui/src/components/ScrollableCard.tsx
new file mode 100644
index 000000000..cb2fc1403
--- /dev/null
+++ b/webui/src/components/ScrollableCard.tsx
@@ -0,0 +1,13 @@
+import { Card, styled } from '@traefiklabs/faency'
+
+const ScrollableCard = styled(Card, {
+ width: '100%',
+ maxHeight: 300,
+ overflowY: 'auto',
+ overflowX: 'hidden',
+ scrollbarWidth: 'thin',
+ scrollbarColor: '$colors-primary $colors-01dp',
+ scrollbarGutter: 'stable',
+})
+
+export default ScrollableCard
diff --git a/webui/src/components/Toast.tsx b/webui/src/components/Toast.tsx
index 81ff31c16..30a836cec 100644
--- a/webui/src/components/Toast.tsx
+++ b/webui/src/components/Toast.tsx
@@ -3,7 +3,7 @@ import { AnimatePresence, motion } from 'framer-motion'
import { ReactNode, useEffect } from 'react'
import { FiX } from 'react-icons/fi'
-import { colorByStatus, iconByStatus, StatusType } from 'components/resources/Status'
+import { colorByStatus, iconByStatus } from 'components/resources/Status'
const CloseButton = styled(Button, {
position: 'absolute',
@@ -39,7 +39,7 @@ const toastVariants = {
}
export type ToastState = {
- severity: StatusType
+ severity: Resource.Status
message?: string
isVisible?: boolean
key?: string
@@ -88,7 +88,7 @@ export const Toast = ({ message, dismiss, severity = 'error', icon, isVisible =
exit="hidden"
variants={toastVariants}
>
- {icon ? icon : propsBySeverity[severity].icon}
+ {icon ? icon : propsBySeverity[severity].icon}
{message}
{!timeout && (
diff --git a/webui/src/components/buttons/CopyButton.tsx b/webui/src/components/buttons/CopyButton.tsx
new file mode 100644
index 000000000..08f60240e
--- /dev/null
+++ b/webui/src/components/buttons/CopyButton.tsx
@@ -0,0 +1,57 @@
+import { Flex, Button, CSS, AccessibleIcon } from '@traefiklabs/faency'
+import React, { useState } from 'react'
+import { FiCheck, FiCopy } from 'react-icons/fi'
+
+type CopyButtonProps = {
+ text: string
+ disabled?: boolean
+ css?: CSS
+ onClick?: () => void
+ iconOnly?: boolean
+ title?: string
+ color?: string
+}
+
+const CopyButton = ({
+ text,
+ disabled,
+ css,
+ onClick,
+ iconOnly = false,
+ title = 'Copy',
+ color = 'var(--colors-textSubtle)',
+}: CopyButtonProps) => {
+ const [showConfirmation, setShowConfirmation] = useState(false)
+
+ return (
+
+ )
+}
+
+export default CopyButton
diff --git a/webui/src/components/ScrollTopButton.tsx b/webui/src/components/buttons/ScrollTopButton.tsx
similarity index 100%
rename from webui/src/components/ScrollTopButton.tsx
rename to webui/src/components/buttons/ScrollTopButton.tsx
diff --git a/webui/src/components/icons/providers/index.tsx b/webui/src/components/icons/providers/index.tsx
index 8745a7644..cb65ebb27 100644
--- a/webui/src/components/icons/providers/index.tsx
+++ b/webui/src/components/icons/providers/index.tsx
@@ -1,3 +1,4 @@
+import { Box } from '@traefiklabs/faency'
import { HTMLAttributes, useMemo } from 'react'
import Consul from 'components/icons/providers/Consul'
@@ -14,13 +15,14 @@ import Nomad from 'components/icons/providers/Nomad'
import Plugin from 'components/icons/providers/Plugin'
import Redis from 'components/icons/providers/Redis'
import Zookeeper from 'components/icons/providers/Zookeeper'
+import Tooltip from 'components/Tooltip'
export type ProviderIconProps = HTMLAttributes & {
height?: number | string
width?: number | string
}
-export default function ProviderIcon({ name, size = 32 }: { name: string; size?: number }) {
+export default function ProviderIcon({ name, size = 20 }: { name: string; size?: number }) {
const Icon = useMemo(() => {
if (!name || typeof name !== 'string') return Internal
@@ -76,3 +78,13 @@ export default function ProviderIcon({ name, size = 32 }: { name: string; size?:
/>
)
}
+
+export const ProviderIconWithTooltip = ({ provider, size = 20 }) => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/webui/src/components/middlewares/MiddlewareDefinition.tsx b/webui/src/components/middlewares/MiddlewareDefinition.tsx
new file mode 100644
index 000000000..5601ce6e0
--- /dev/null
+++ b/webui/src/components/middlewares/MiddlewareDefinition.tsx
@@ -0,0 +1,40 @@
+import { useMemo } from 'react'
+
+import ProviderIcon from 'components/icons/providers'
+import { ProviderName } from 'components/resources/DetailItemComponents'
+import DetailsCard from 'components/resources/DetailsCard'
+import { ResourceStatus } from 'components/resources/ResourceStatus'
+import { parseMiddlewareType } from 'libs/parsers'
+
+type MiddlewareDefinitionProps = {
+ data: Middleware.Details
+ testId?: string
+}
+
+const MiddlewareDefinition = ({ data, testId }: MiddlewareDefinitionProps) => {
+ const providerName = useMemo(() => {
+ return data.provider
+ }, [data.provider])
+
+ const detailsItems = useMemo(
+ () =>
+ [
+ data.status && { key: 'Status', val: },
+ (data.type || data.plugin) && { key: 'Type', val: parseMiddlewareType(data) },
+ data.provider && {
+ key: 'Provider',
+ val: (
+ <>
+
+ {providerName}
+ >
+ ),
+ },
+ ].filter(Boolean) as { key: string; val: string | React.ReactElement }[],
+ [data, providerName],
+ )
+
+ return
+}
+
+export default MiddlewareDefinition
diff --git a/webui/src/components/middlewares/MiddlewareDetail.tsx b/webui/src/components/middlewares/MiddlewareDetail.tsx
new file mode 100644
index 000000000..14ca8f290
--- /dev/null
+++ b/webui/src/components/middlewares/MiddlewareDetail.tsx
@@ -0,0 +1,100 @@
+import { Card, Flex, H1, Skeleton, Text } from '@traefiklabs/faency'
+import { useMemo } from 'react'
+import { Helmet } from 'react-helmet-async'
+
+import MiddlewareDefinition from './MiddlewareDefinition'
+import { RenderUnknownProp } from './RenderUnknownProp'
+
+import { DetailsCardSkeleton } from 'components/resources/DetailsCard'
+import ResourceErrors, { ResourceErrorsSkeleton } from 'components/resources/ResourceErrors'
+import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection'
+import { NotFound } from 'pages/NotFound'
+
+type MiddlewareDetailProps = {
+ data?: Resource.DetailsData
+ error?: Error | null
+ name: string
+ protocol: 'http' | 'tcp'
+}
+
+const filterMiddlewareProps = (middleware: Middleware.Details): string[] => {
+ const filteredProps = [] as string[]
+ const propsToRemove = ['name', 'plugin', 'status', 'type', 'provider', 'error', 'usedBy', 'routers']
+
+ Object.keys(middleware).map((propName) => {
+ if (!propsToRemove.includes(propName)) {
+ filteredProps.push(propName)
+ }
+ })
+
+ return filteredProps
+}
+
+export const MiddlewareDetail = ({ data, error, name, protocol }: MiddlewareDetailProps) => {
+ const filteredProps = useMemo(() => {
+ if (data) {
+ return filterMiddlewareProps(data)
+ }
+
+ return []
+ }, [data])
+
+ if (error) {
+ return (
+ <>
+
+ {name} - Traefik Proxy
+
+
+ Sorry, we could not fetch detail information for this Middleware right now. Please, try again later.
+
+ >
+ )
+ }
+
+ if (!data) {
+ return (
+ <>
+
+ {name} - Traefik Proxy
+
+
+
+
+
+
+
+ >
+ )
+ }
+
+ if (!data.name) {
+ return
+ }
+
+ return (
+ <>
+
+ {data.name} - Traefik Proxy
+
+ {data.name}
+
+
+ {!!data.error && }
+ {(!!data.plugin || !!filteredProps.length) && (
+
+ {data.plugin &&
+ Object.keys(data.plugin).map((pluginName) => (
+
+ ))}
+ {filteredProps?.map((propName) => (
+
+ ))}
+
+ )}
+
+
+
+ >
+ )
+}
diff --git a/webui/src/components/resources/RenderUnknownProp.spec.tsx b/webui/src/components/middlewares/RenderUnknownProp.spec.tsx
similarity index 100%
rename from webui/src/components/resources/RenderUnknownProp.spec.tsx
rename to webui/src/components/middlewares/RenderUnknownProp.spec.tsx
diff --git a/webui/src/components/resources/RenderUnknownProp.tsx b/webui/src/components/middlewares/RenderUnknownProp.tsx
similarity index 74%
rename from webui/src/components/resources/RenderUnknownProp.tsx
rename to webui/src/components/middlewares/RenderUnknownProp.tsx
index e4f0d118e..40520d33e 100644
--- a/webui/src/components/resources/RenderUnknownProp.tsx
+++ b/webui/src/components/middlewares/RenderUnknownProp.tsx
@@ -1,11 +1,9 @@
-import { Text } from '@traefiklabs/faency'
import { ReactNode } from 'react'
-import { BooleanState, ItemBlock } from './DetailSections'
-import GenericTable from './GenericTable'
-import IpStrategyTable, { IpStrategy } from './IpStrategyTable'
-
-import Tooltip from 'components/Tooltip'
+import CopyableText from 'components/CopyableText'
+import { BooleanState, ItemBlock } from 'components/resources/DetailItemComponents'
+import GenericTable from 'components/resources/GenericTable'
+import IpStrategyTable, { IpStrategy } from 'components/resources/IpStrategyTable'
type RenderUnknownPropProps = {
name: string
@@ -22,23 +20,19 @@ export const RenderUnknownProp = ({ name, prop, removeTitlePrefix }: RenderUnkno
try {
if (typeof prop !== 'undefined') {
if (typeof prop === 'boolean') {
- return wrap()
+ return wrap()
}
if (typeof prop === 'string' && ['true', 'false'].includes((prop as string).toLowerCase())) {
- return wrap()
+ return wrap()
}
if (['string', 'number'].includes(typeof prop)) {
- return wrap(
-
- {prop as string}
- ,
- )
+ return wrap()
}
if (JSON.stringify(prop) === '{}') {
- return wrap()
+ return wrap()
}
if (prop instanceof Array) {
@@ -75,7 +69,7 @@ export const RenderUnknownProp = ({ name, prop, removeTitlePrefix }: RenderUnkno
}
}
} catch (error) {
- console.log('Unable to render plugin property:', { name, prop }, { error })
+ console.error('Unable to render plugin property:', { name, prop }, { error })
}
return null
diff --git a/webui/src/components/resources/AdditionalFeatures.spec.tsx b/webui/src/components/resources/AdditionalFeatures.spec.tsx
deleted file mode 100644
index d1c448b19..000000000
--- a/webui/src/components/resources/AdditionalFeatures.spec.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import AdditionalFeatures from './AdditionalFeatures'
-
-import { MiddlewareProps } from 'hooks/use-resource-detail'
-import { renderWithProviders } from 'utils/test'
-
-describe('', () => {
- it('should render the middleware info', () => {
- renderWithProviders()
- })
-
- it('should render the middleware info with number', () => {
- const middlewares: MiddlewareProps[] = [
- {
- retry: {
- attempts: 2,
- },
- },
- ]
-
- const { container } = renderWithProviders()
-
- expect(container.innerHTML).toContain('Retry: Attempts=2')
- })
-
- it('should render the middleware info with string', () => {
- const middlewares: MiddlewareProps[] = [
- {
- circuitBreaker: {
- expression: 'expression',
- },
- },
- ]
-
- const { container } = renderWithProviders()
-
- expect(container.innerHTML).toContain('CircuitBreaker: Expression="expression"')
- })
-
- it('should render the middleware info with string', () => {
- const middlewares: MiddlewareProps[] = [
- {
- rateLimit: {
- burst: 100,
- average: 100,
- },
- },
- ]
-
- const { container } = renderWithProviders()
-
- expect(container.innerHTML).toContain('RateLimit: Burst=100, Average=100')
- })
-})
diff --git a/webui/src/components/resources/AdditionalFeatures.tsx b/webui/src/components/resources/AdditionalFeatures.tsx
deleted file mode 100644
index 2b95db582..000000000
--- a/webui/src/components/resources/AdditionalFeatures.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { Badge, Box, Text } from '@traefiklabs/faency'
-
-import Tooltip from 'components/Tooltip'
-import { MiddlewareProps, ValuesMapType } from 'hooks/use-resource-detail'
-
-function capitalize(word: string): string {
- return word.charAt(0).toUpperCase() + word.slice(1)
-}
-
-function quote(value: string | number): string | number {
- if (typeof value === 'string') {
- return `"${value}"`
- }
-
- return value
-}
-
-function quoteArray(values: (string | number)[]): (string | number)[] {
- return values.map(quote)
-}
-
-const renderFeatureValues = (valuesMap: ValuesMapType): string => {
- return Object.entries(valuesMap)
- .map(([name, value]) => {
- const capitalizedName = capitalize(name)
- if (typeof value === 'string') {
- return [capitalizedName, `"${value}"`].join('=')
- }
-
- if (value instanceof Array) {
- return [capitalizedName, quoteArray(value).join(', ')].join('=')
- }
-
- if (typeof value === 'object') {
- return [capitalizedName, `{${renderFeatureValues(value)}}`].join('=')
- }
-
- return [capitalizedName, value].join('=')
- })
- .join(', ')
-}
-
-const FeatureMiddleware = ({ middleware }: { middleware: MiddlewareProps }) => {
- const [name, value] = Object.entries(middleware)[0]
- const content = `${capitalize(name)}: ${renderFeatureValues(value)}`
-
- return (
-
-
- {content}
-
-
- )
-}
-
-type AdditionalFeaturesProps = {
- middlewares?: MiddlewareProps[]
- uid: string
-}
-
-const AdditionalFeatures = ({ middlewares, uid }: AdditionalFeaturesProps) => {
- return middlewares?.length ? (
-
- {middlewares.map((m, idx) => (
-
- ))}
-
- ) : (
- No additional features
- )
-}
-
-export default AdditionalFeatures
diff --git a/webui/src/components/resources/DetailItemComponents.tsx b/webui/src/components/resources/DetailItemComponents.tsx
new file mode 100644
index 000000000..b4891216a
--- /dev/null
+++ b/webui/src/components/resources/DetailItemComponents.tsx
@@ -0,0 +1,95 @@
+import { Badge, CSS, Flex, styled, Text } from '@traefiklabs/faency'
+import { ReactNode } from 'react'
+import { BsToggleOff, BsToggleOn } from 'react-icons/bs'
+
+import { colorByStatus } from './Status'
+
+import CopyButton from 'components/buttons/CopyButton'
+
+export const ItemTitle = styled(Text, {
+ marginBottom: '$3',
+ color: 'hsl(0, 0%, 56%)',
+ fontSize: '12px',
+ fontWeight: 600,
+ textAlign: 'left',
+ textTransform: 'capitalize',
+ wordBreak: 'break-word',
+})
+
+const ItemBlockContainer = styled(Flex, {
+ maxWidth: '100%',
+ flexWrap: 'wrap !important',
+ rowGap: '$2',
+
+ // This forces the Tooltips to respect max-width, since we can't define
+ // it directly on the component, and the Chips are automatically covered.
+ span: {
+ maxWidth: '100%',
+ },
+})
+
+const FlexLimited = styled(Flex, {
+ maxWidth: '100%',
+ margin: '0 -8px -8px 0',
+ span: {
+ maxWidth: '100%',
+ },
+})
+
+type ChipsType = {
+ items: string[]
+ variant?: 'gray' | 'red' | 'blue' | 'green' | 'neon' | 'orange' | 'purple'
+ alignment?: 'center' | 'left'
+}
+
+export const Chips = ({ items, variant, alignment = 'left' }: ChipsType) => (
+
+ {items.map((item, index) => (
+
+
+ {item}
+
+
+
+ ))}
+
+)
+
+type ItemBlockType = {
+ title: string
+ children?: ReactNode
+}
+
+export const ItemBlock = ({ title, children }: ItemBlockType) => (
+
+ {title}
+ {children}
+
+)
+
+export const BooleanState = ({ enabled, css }: { enabled: boolean; css?: CSS }) => (
+
+ {enabled ? (
+
+ ) : (
+
+ )}
+
+
+ {enabled ? 'True' : 'False'}
+
+
+)
+
+export const ProviderName = styled(Text, {
+ textTransform: 'capitalize',
+ overflowWrap: 'break-word',
+ fontSize: 'inherit !important',
+})
+
+export const EmptyPlaceholder = styled(Text, {
+ color: 'hsl(0, 0%, 76%)',
+ fontSize: '20px',
+ fontWeight: '700',
+ lineHeight: '1.2',
+})
diff --git a/webui/src/components/resources/DetailSections.tsx b/webui/src/components/resources/DetailSections.tsx
deleted file mode 100644
index 53e1ee670..000000000
--- a/webui/src/components/resources/DetailSections.tsx
+++ /dev/null
@@ -1,356 +0,0 @@
-import { Badge, Box, Card, Flex, H2, styled, Text } from '@traefiklabs/faency'
-import { ReactNode } from 'react'
-import { FiArrowRight, FiToggleLeft, FiToggleRight } from 'react-icons/fi'
-import { useNavigate } from 'react-router-dom'
-
-import { StatusWrapper } from './ResourceStatus'
-import { colorByStatus } from './Status'
-
-import Tooltip from 'components/Tooltip'
-import { useGetUrlWithReturnTo } from 'hooks/use-href-with-return-to'
-
-const CustomHeading = styled(H2, {
- display: 'flex',
- alignItems: 'center',
-})
-
-type SectionHeaderType = {
- icon?: ReactNode
- title?: string | undefined
-}
-
-export const SectionHeader = ({ icon, title }: SectionHeaderType) => {
- if (!title) {
- return (
-
-
-
-
- )
- }
-
- return (
-
- {icon ? icon : null}
-
- {title}
-
-
- )
-}
-
-export const ItemTitle = styled(Text, {
- marginBottom: '$3',
- color: 'hsl(0, 0%, 56%)',
- letterSpacing: '3px',
- fontSize: '12px',
- fontWeight: 600,
- textAlign: 'left',
- textTransform: 'uppercase',
- wordBreak: 'break-word',
-})
-
-const SpacedCard = styled(Card, {
- '& + &': {
- marginTop: '16px',
- },
-})
-
-const CardDescription = styled(Text, {
- textAlign: 'left',
- fontWeight: '700',
- fontSize: '16px',
- lineHeight: '16px',
- wordBreak: 'break-word',
-})
-
-const CardListColumnWrapper = styled(Flex, {
- display: 'flex',
-})
-
-const CardListColumn = styled(Flex, {
- minWidth: 160,
- maxWidth: '66%',
- maxHeight: '416px',
- overflowY: 'auto',
- p: '$1',
-})
-
-const ItemBlockContainer = styled(Flex, {
- maxWidth: '100%',
- flexWrap: 'wrap !important',
- rowGap: '$2',
-
- // This forces the Tooltips to respect max-width, since we can't define
- // it directly on the component, and the Chips are automatically covered.
- span: {
- maxWidth: '100%',
- },
-})
-
-const FlexLink = styled('a', {
- display: 'flex',
- flexFlow: 'column',
- textDecoration: 'none',
-})
-
-type CardType = {
- title: string
- description?: string
- focus?: boolean
- link?: string
-}
-
-type SectionType = SectionHeaderType & {
- cards?: CardType[] | undefined
- isLast?: boolean
- bigDescription?: boolean
-}
-const CardSkeleton = ({ bigDescription }: { bigDescription?: boolean }) => {
- return (
-
-
-
-
-
-
-
-
- )
-}
-
-const CardItem = ({ card }) => {
- const navigate = useNavigate()
- const href = useGetUrlWithReturnTo(card.link)
-
- return (
-
- !!card.link && navigate(href)}
- css={{ cursor: card.link ? 'pointer' : 'inherit' }}
- >
- {card.title}
- {card.description}
-
-
- )
-}
-
-export const CardListSection = ({ icon, title, cards, isLast, bigDescription }: SectionType) => {
- return (
-
-
-
-
-
- {!cards && }
- {cards?.filter((c) => !!c.description).map((card, idx) => )}
-
-
-
- {!isLast && (
-
-
-
- )}
-
-
- )
-}
-
-const FlexCard = styled(Card, {
- display: 'flex',
- flexFlow: 'column',
- flexGrow: '1',
- overflowY: 'auto',
- height: '600px',
-})
-
-const NarrowFlexCard = styled(FlexCard, {
- height: '400px',
-})
-
-const ItemTitleSkeleton = styled(Box, {
- height: '16px',
- backgroundColor: '$slate5',
- borderRadius: '3px',
-})
-
-const ItemDescriptionSkeleton = styled(Box, {
- height: '16px',
- backgroundColor: '$slate5',
- borderRadius: '3px',
-})
-
-type DetailSectionSkeletonType = {
- narrow?: boolean
-}
-
-export const DetailSectionSkeleton = ({ narrow }: DetailSectionSkeletonType) => {
- const Card = narrow ? NarrowFlexCard : FlexCard
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-type DetailSectionType = SectionHeaderType & {
- children?: ReactNode
- noPadding?: boolean
- narrow?: boolean
-}
-
-export const DetailSection = ({ icon, title, children, narrow, noPadding }: DetailSectionType) => {
- const Card = narrow ? NarrowFlexCard : FlexCard
-
- return (
-
-
- {children}
-
- )
-}
-
-const FlexLimited = styled(Flex, {
- maxWidth: '100%',
- margin: '0 -8px -8px 0',
- span: {
- maxWidth: '100%',
- },
-})
-
-type ChipsType = {
- items: string[]
- variant?: 'gray' | 'red' | 'blue' | 'green' | 'neon' | 'orange' | 'purple'
- alignment?: 'center' | 'left'
-}
-
-export const Chips = ({ items, variant, alignment = 'left' }: ChipsType) => (
-
- {items.map((item, index) => (
-
-
- {item}
-
-
- ))}
-
-)
-
-type ChipPropsListType = {
- data: {
- [key: string]: string
- }
- variant?: 'gray' | 'red' | 'blue' | 'green' | 'neon' | 'orange' | 'purple'
-}
-
-export const ChipPropsList = ({ data, variant }: ChipPropsListType) => (
-
- {Object.entries(data).map((entry: [string, string]) => (
-
- {entry[1]}
-
- ))}
-
-)
-
-type ItemBlockType = {
- title: string
- children?: ReactNode
-}
-
-export const ItemBlock = ({ title, children }: ItemBlockType) => (
-
- {title}
- {children}
-
-)
-
-const LayoutCols = styled(Box, {
- display: 'grid',
- gridGap: '16px',
-})
-
-export const LayoutTwoCols = styled(LayoutCols, {
- gridTemplateColumns: 'repeat(2, minmax(50%, 1fr))',
-})
-
-export const LayoutThreeCols = styled(LayoutCols, {
- gridTemplateColumns: 'repeat(3, minmax(30%, 1fr))',
-})
-
-export const BooleanState = ({ enabled }: { enabled: boolean }) => (
-
-
- {enabled ? : }
-
-
- {enabled ? 'True' : 'False'}
-
-
-)
-
-export const ProviderName = styled(Text, {
- textTransform: 'capitalize',
- overflowWrap: 'break-word',
-})
-
-export const EmptyPlaceholder = styled(Text, {
- color: 'hsl(0, 0%, 76%)',
- fontSize: '20px',
- fontWeight: '700',
- lineHeight: '1.2',
-})
diff --git a/webui/src/components/resources/DetailsCard.tsx b/webui/src/components/resources/DetailsCard.tsx
new file mode 100644
index 000000000..463e0c095
--- /dev/null
+++ b/webui/src/components/resources/DetailsCard.tsx
@@ -0,0 +1,207 @@
+import { Card, CSS, Flex, Grid, H2, Skeleton, styled, Text } from '@traefiklabs/faency'
+import { Fragment, ReactNode, useMemo } from 'react'
+
+import ScrollableCard from 'components/ScrollableCard'
+import breakpoints from 'utils/breakpoints'
+
+const StyledText = styled(Text, {
+ fontSize: 'inherit !important',
+ lineHeight: '24px',
+})
+
+export const ValText = styled(StyledText, {
+ overflowWrap: 'break-word',
+ wordBreak: 'break-word',
+})
+
+export const SectionTitle = ({ icon, title }: { icon?: ReactNode; title: string }) => {
+ return (
+
+ {icon && icon}
+ {title}
+
+ )
+}
+
+type DetailsCardProps = {
+ css?: CSS
+ keyColumns?: number
+ items: { key: string; val: string | React.ReactElement; stackVertical?: boolean; forceNewRow?: boolean }[]
+ minKeyWidth?: string
+ maxKeyWidth?: string
+ testidPrefix?: string
+ testId?: string
+ title?: string
+ icon?: ReactNode
+ scrollable?: boolean
+}
+
+export default function DetailsCard({
+ css = {},
+ keyColumns = 2,
+ items,
+ minKeyWidth,
+ maxKeyWidth,
+ testidPrefix = 'definition',
+ testId,
+ title,
+ icon,
+ scrollable = false,
+}: DetailsCardProps) {
+ const ParentComponent = useMemo(() => {
+ if (scrollable) {
+ return ScrollableCard
+ }
+ return Card
+ }, [scrollable])
+
+ return (
+
+ {title ? : null}
+
+
+ {items.map((item, index) => {
+ // Handle forceNewRow props
+ const cellsBeforeThis = items.slice(0, index).reduce((count, prevItem) => {
+ if (prevItem.stackVertical) return count + keyColumns
+ return count + 1
+ }, 0)
+
+ const needsEmptyCell = item.forceNewRow && cellsBeforeThis % keyColumns !== 0
+
+ return (
+
+ {needsEmptyCell && (
+ <>
+
+
+ >
+ )}
+ {item.stackVertical ? (
+
+
+ {item.key}
+
+ {typeof item.val === 'string' ? (
+ {item.val}
+ ) : (
+ *': {
+ height: 'fit-content',
+ },
+ height: '100%',
+ }}
+ >
+ {item.val}
+
+ )}
+
+ ) : (
+ <>
+
+ {index < keyColumns
+ ? items
+ .filter((hiddenItem) => hiddenItem.key != item.key)
+ .map((hiddenItem, jndex) => (
+
+ {hiddenItem.key}
+
+ ))
+ : null}
+
+ {item.key}
+
+
+ {typeof item.val === 'string' ? (
+ {item.val}
+ ) : (
+ *': {
+ height: 'fit-content',
+ },
+ height: '100%',
+ }}
+ >
+ {item.val}
+
+ )}
+ >
+ )}
+
+ )
+ })}
+
+
+
+ )
+}
+
+export function DetailsCardSkeleton({
+ keyColumns = 2,
+ rows = 3,
+ testidPrefix = 'definition',
+ title,
+ icon,
+}: { rows?: number } & Omit) {
+ return (
+
+ {title ? : }
+
+
+ {[...Array(rows * keyColumns)].map((_, idx) => (
+
+
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/webui/src/components/resources/GenericTable.tsx b/webui/src/components/resources/GenericTable.tsx
index 4ebacbefd..7dce66af5 100644
--- a/webui/src/components/resources/GenericTable.tsx
+++ b/webui/src/components/resources/GenericTable.tsx
@@ -1,16 +1,17 @@
import { AriaTable, AriaTbody, AriaTd, AriaTr, Flex, Text } from '@traefiklabs/faency'
import { useMemo } from 'react'
-import Status, { StatusType } from './Status'
+import Status from './Status'
-import Tooltip from 'components/Tooltip'
+import CopyableText from 'components/CopyableText'
type GenericTableProps = {
items: (number | string)[]
- status?: StatusType
+ status?: Resource.Status
+ copyable?: boolean
}
-export default function GenericTable({ items, status }: GenericTableProps) {
+export default function GenericTable({ items, status, copyable = false }: GenericTableProps) {
const border = useMemo(() => `1px solid $${status === 'error' ? 'textRed' : 'tableRowBorder'}`, [status])
return (
@@ -19,23 +20,31 @@ export default function GenericTable({ items, status }: GenericTableProps) {
{items.map((item, index) => (
-
-
- {status ? (
-
- ) : (
-
- {index}
-
- )}
+
+ {status ? (
+
+ ) : (
+
+ {index}
+
+ )}
+ {copyable ? (
+
+ ) : (
{item}
-
-
+ )}
+
))}
diff --git a/webui/src/components/resources/MiddlewarePanel.tsx b/webui/src/components/resources/MiddlewarePanel.tsx
deleted file mode 100644
index 37806ac0d..000000000
--- a/webui/src/components/resources/MiddlewarePanel.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { Box, Flex, H3, styled, Text } from '@traefiklabs/faency'
-import { FiLayers } from 'react-icons/fi'
-
-import { DetailSection, EmptyPlaceholder, ItemBlock, LayoutTwoCols, ProviderName } from './DetailSections'
-import GenericTable from './GenericTable'
-import { RenderUnknownProp } from './RenderUnknownProp'
-import { ResourceStatus } from './ResourceStatus'
-
-import { EmptyIcon } from 'components/icons/EmptyIcon'
-import ProviderIcon from 'components/icons/providers'
-import { Middleware, RouterDetailType } from 'hooks/use-resource-detail'
-import { parseMiddlewareType } from 'libs/parsers'
-
-const Separator = styled('hr', {
- border: 'none',
- background: '$tableRowBorder',
- margin: '0 0 24px',
- height: '1px',
- minHeight: '1px',
-})
-
-const filterMiddlewareProps = (middleware: Middleware): string[] => {
- const filteredProps = [] as string[]
- const propsToRemove = ['name', 'plugin', 'status', 'type', 'provider', 'error', 'usedBy', 'routers']
-
- Object.keys(middleware).map((propName) => {
- if (!propsToRemove.includes(propName)) {
- filteredProps.push(propName)
- }
- })
-
- return filteredProps
-}
-
-type RenderMiddlewareProps = {
- middleware: Middleware
- withHeader?: boolean
-}
-
-export const RenderMiddleware = ({ middleware, withHeader }: RenderMiddlewareProps) => (
-
- {withHeader && {middleware.name}
}
-
- {(middleware.type || middleware.plugin) && (
-
- {parseMiddlewareType(middleware)}
-
- )}
- {middleware.provider && (
-
-
- {middleware.provider}
-
- )}
-
- {middleware.status && (
-
-
-
- )}
- {middleware.error && (
-
-
-
- )}
- {middleware.plugin &&
- Object.keys(middleware.plugin).map((pluginName) => (
-
- ))}
- {filterMiddlewareProps(middleware).map((propName) => (
-
- ))}
-
-)
-
-const MiddlewarePanel = ({ data }: { data: RouterDetailType }) => (
- } title="Middlewares">
- {data.middlewares ? (
- data.middlewares.map((middleware, index) => (
-
-
- {data.middlewares && index < data.middlewares.length - 1 && }
-
- ))
- ) : (
-
-
-
-
-
- There are no
-
- Middlewares configured
-
-
- )}
-
-)
-
-export default MiddlewarePanel
diff --git a/webui/src/components/resources/ResourceErrors.tsx b/webui/src/components/resources/ResourceErrors.tsx
new file mode 100644
index 000000000..e4ba60aa2
--- /dev/null
+++ b/webui/src/components/resources/ResourceErrors.tsx
@@ -0,0 +1,31 @@
+import { Card, Flex, Skeleton } from '@traefiklabs/faency'
+import { FiAlertTriangle } from 'react-icons/fi'
+
+import { SectionTitle } from './DetailsCard'
+import GenericTable from './GenericTable'
+
+const ResourceErrors = ({ errors }: { errors: string[] }) => {
+ return (
+
+ } />
+
+
+
+
+ )
+}
+
+export const ResourceErrorsSkeleton = () => {
+ return (
+
+
+
+ {[...Array(4)].map((_, idx) => (
+
+ ))}
+
+
+ )
+}
+
+export default ResourceErrors
diff --git a/webui/src/components/resources/ResourceStatus.tsx b/webui/src/components/resources/ResourceStatus.tsx
index 51e3a7e81..d50040b5b 100644
--- a/webui/src/components/resources/ResourceStatus.tsx
+++ b/webui/src/components/resources/ResourceStatus.tsx
@@ -1,25 +1,26 @@
-import { Flex, styled, Text } from '@traefiklabs/faency'
+import { Box, Flex, styled, Text } from '@traefiklabs/faency'
import { ReactNode } from 'react'
-import { colorByStatus, iconByStatus, StatusType } from 'components/resources/Status'
+import { colorByStatus, iconByStatus } from 'components/resources/Status'
export const StatusWrapper = styled(Flex, {
- height: '32px',
- width: '32px',
+ height: '24px',
+ width: '24px',
padding: 0,
borderRadius: '4px',
})
type Props = {
- status: StatusType
+ status: Resource.Status
label?: string
withLabel?: boolean
+ size?: number
}
type Value = { color: string; icon: ReactNode; label: string }
-export const ResourceStatus = ({ status, withLabel = false }: Props) => {
- const valuesByStatus: { [key in StatusType]: Value } = {
+export const ResourceStatus = ({ status, withLabel = false, size = 20 }: Props) => {
+ const valuesByStatus: { [key in Resource.Status]: Value } = {
info: {
color: colorByStatus.info,
icon: iconByStatus.info,
@@ -50,6 +51,11 @@ export const ResourceStatus = ({ status, withLabel = false }: Props) => {
icon: iconByStatus.disabled,
label: 'Error',
},
+ loading: {
+ color: colorByStatus.loading,
+ icon: iconByStatus.loading,
+ label: 'Loading...',
+ },
}
const values = valuesByStatus[status]
@@ -59,12 +65,12 @@ export const ResourceStatus = ({ status, withLabel = false }: Props) => {
}
return (
-
-
- {values.icon}
-
+
+ {values.icon}
{withLabel && values.label && (
- {values.label}
+
+ {values.label}
+
)}
)
diff --git a/webui/src/components/resources/RouterPanel.tsx b/webui/src/components/resources/RouterPanel.tsx
deleted file mode 100644
index c380df50e..000000000
--- a/webui/src/components/resources/RouterPanel.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Badge, Text } from '@traefiklabs/faency'
-import { FiInfo } from 'react-icons/fi'
-
-import { DetailSection, ItemBlock, LayoutTwoCols, ProviderName } from './DetailSections'
-import GenericTable from './GenericTable'
-import { ResourceStatus } from './ResourceStatus'
-
-import ProviderIcon from 'components/icons/providers'
-import Tooltip from 'components/Tooltip'
-import { ResourceDetailDataType } from 'hooks/use-resource-detail'
-
-type Props = {
- data: ResourceDetailDataType
-}
-
-const RouterPanel = ({ data }: Props) => (
- } title="Router Details">
-
- {data.status && (
-
-
-
- )}
- {data.provider && (
-
-
- {data.provider}
-
- )}
- {data.priority && (
-
-
- {data.priority.toString()}
-
-
- )}
-
- {data.rule ? (
-
-
- {data.rule}
-
-
- ) : null}
- {data.name && (
-
-
- {data.name}
-
-
- )}
- {!!data.using && data.using && data.using.length > 0 && (
-
- {data.using.map((ep) => (
-
- {ep}
-
- ))}
-
- )}
- {data.service && (
-
-
- {data.service}
-
-
- )}
- {data.error && (
-
-
-
- )}
-
-)
-
-export default RouterPanel
diff --git a/webui/src/components/resources/Status.tsx b/webui/src/components/resources/Status.tsx
index d67c126e9..d90799f58 100644
--- a/webui/src/components/resources/Status.tsx
+++ b/webui/src/components/resources/Status.tsx
@@ -1,49 +1,50 @@
import { Box, CSS } from '@traefiklabs/faency'
import { ReactNode } from 'react'
-import { FiAlertCircle, FiAlertTriangle, FiCheckCircle } from 'react-icons/fi'
+import { FiAlertCircle, FiAlertTriangle, FiCheckCircle, FiLoader } from 'react-icons/fi'
-export type StatusType = 'info' | 'success' | 'warning' | 'error' | 'enabled' | 'disabled'
-
-export const iconByStatus: { [key in StatusType]: ReactNode } = {
- info: ,
- success: ,
- warning: ,
- error: ,
- enabled: ,
- disabled: ,
+export const iconByStatus: { [key in Resource.Status]: ReactNode } = {
+ info: ,
+ success: ,
+ warning: ,
+ error: ,
+ enabled: ,
+ disabled: ,
+ loading: ,
}
// Please notice: dark and light colors have the same values.
-export const colorByStatus: { [key in StatusType]: string } = {
+export const colorByStatus: { [key in Resource.Status]: string } = {
info: 'hsl(220, 67%, 51%)',
success: '#30A46C',
warning: 'hsl(24 94.0% 50.0%)',
error: 'hsl(347, 100%, 60.0%)',
enabled: '#30A46C',
disabled: 'hsl(347, 100%, 60.0%)',
+ loading: 'hsla(0, 0%, 100%, 0.51)',
}
type StatusProps = {
css?: CSS
size?: number
- status: StatusType
+ status: Resource.Status
+ color?: string
}
-export default function Status({ css = {}, size = 20, status }: StatusProps) {
+export default function Status({ css = {}, size = 20, status, color = 'white' }: StatusProps) {
const Icon = ({ size }: { size: number }) => {
switch (status) {
case 'info':
- return
+ return
case 'success':
- return
+ return
case 'warning':
- return
+ return
case 'error':
- return
+ return
case 'enabled':
- return
+ return
case 'disabled':
- return
+ return
default:
return null
}
diff --git a/webui/src/components/resources/TlsPanel.tsx b/webui/src/components/resources/TlsPanel.tsx
deleted file mode 100644
index a5890d707..000000000
--- a/webui/src/components/resources/TlsPanel.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { Badge, Box, Flex, Text } from '@traefiklabs/faency'
-import { FiShield } from 'react-icons/fi'
-
-import { BooleanState, DetailSection, EmptyPlaceholder, ItemBlock } from './DetailSections'
-
-import { EmptyIcon } from 'components/icons/EmptyIcon'
-import { RouterDetailType } from 'hooks/use-resource-detail'
-
-type Props = {
- data: RouterDetailType
-}
-
-const TlsPanel = ({ data }: Props) => (
- } title="TLS">
- {data.tls ? (
-
-
-
-
- {data.tls.options && (
-
- {data.tls.options}
-
- )}
-
-
-
- {data.tls.certResolver && (
-
- {data.tls.certResolver}
-
- )}
- {data.tls.domains && (
-
-
- {data.tls.domains?.map((domain) => (
-
-
-
- {domain.main}
-
-
- {domain.sans?.map((sub) => (
-
- {sub}
-
- ))}
-
- ))}
-
-
- )}
-
- ) : (
-
-
-
-
-
- There is no
-
- TLS configured
-
-
- )}
-
-)
-
-export default TlsPanel
diff --git a/webui/src/components/resources/TraefikResourceStatsCard.tsx b/webui/src/components/resources/TraefikResourceStatsCard.tsx
index 15dfa4508..60c59812d 100644
--- a/webui/src/components/resources/TraefikResourceStatsCard.tsx
+++ b/webui/src/components/resources/TraefikResourceStatsCard.tsx
@@ -5,7 +5,7 @@ import { Doughnut } from 'react-chartjs-2'
import { FaArrowRightLong } from 'react-icons/fa6'
import { Link as RouterLink, useNavigate } from 'react-router-dom'
-import Status, { colorByStatus, StatusType } from './Status'
+import Status, { colorByStatus } from './Status'
import { capitalizeFirstLetter } from 'utils/string'
@@ -58,7 +58,7 @@ export type DataType = {
const getPercent = (total: number, value: number) => (total > 0 ? ((value * 100) / total).toFixed(0) : 0)
-const STATS_ATTRIBUTES: { status: StatusType; label: string }[] = [
+const STATS_ATTRIBUTES: { status: Resource.Status; label: string }[] = [
{
status: 'enabled',
label: 'success',
@@ -80,7 +80,7 @@ const CustomLegend = ({
total,
linkTo,
}: {
- status: StatusType
+ status: Resource.Status
label: string
count: number
total: number
diff --git a/webui/src/components/resources/UsedByRoutersSection.tsx b/webui/src/components/resources/UsedByRoutersSection.tsx
index 5eec1e465..a80c630ee 100644
--- a/webui/src/components/resources/UsedByRoutersSection.tsx
+++ b/webui/src/components/resources/UsedByRoutersSection.tsx
@@ -1,96 +1,24 @@
-import { AriaTable, AriaTbody, AriaTd, AriaTh, AriaThead, AriaTr, Box, Flex, styled } from '@traefiklabs/faency'
+import { Flex } from '@traefiklabs/faency'
import { orderBy } from 'lodash'
import { useContext, useEffect, useMemo } from 'react'
import { useSearchParams } from 'react-router-dom'
-import { SectionHeader } from 'components/resources/DetailSections'
-import SortableTh from 'components/tables/SortableTh'
+import { SectionTitle } from './DetailsCard'
+
+import AriaTableSkeleton from 'components/tables/AriaTableSkeleton'
+import PaginatedTable from 'components/tables/PaginatedTable'
import { ToastContext } from 'contexts/toasts'
-import { MiddlewareDetailType, ServiceDetailType } from 'hooks/use-resource-detail'
import { makeRowRender } from 'pages/http/HttpRouters'
type UsedByRoutersSectionProps = {
- data: ServiceDetailType | MiddlewareDetailType
+ data: Service.Details | Middleware.DetailsData
protocol?: string
}
-const SkeletonContent = styled(Box, {
- backgroundColor: '$slate5',
- height: '14px',
- minWidth: '50px',
- borderRadius: '4px',
- margin: '8px',
-})
-
export const UsedByRoutersSkeleton = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
)
@@ -118,29 +46,38 @@ export const UsedByRoutersSection = ({ data, protocol = 'http' }: UsedByRoutersS
)
}, [addToast, routersNotFound])
+ const columns = useMemo((): Array<{
+ key: keyof Router.DetailsData
+ header: string
+ sortable?: boolean
+ width?: string
+ }> => {
+ return [
+ { key: 'status', header: 'Status', sortable: true, width: '36px' },
+ ...(protocol !== 'udp' ? [{ key: 'tls' as keyof Router.DetailsData, header: 'TLS', width: '24px' }] : []),
+ ...(protocol !== 'udp' ? [{ key: 'rule' as keyof Router.DetailsData, header: 'Rule', sortable: true }] : []),
+ { key: 'using', header: 'Entrypoints', sortable: true },
+ { key: 'name', header: 'Name', sortable: true },
+ { key: 'service', header: 'Service', sortable: true },
+ { key: 'provider', header: 'Provider', sortable: true, width: '40px' },
+ { key: 'priority', header: 'Priority', sortable: true },
+ ]
+ }, [protocol])
+
if (!routersFound || routersFound.length <= 0) {
return null
}
return (
-
-
-
-
-
-
-
- {protocol !== 'udp' ? : null}
- {protocol !== 'udp' ? : null}
-
-
-
-
-
-
-
- {routersFound.map(renderRow)}
-
+
+
+
)
}
diff --git a/webui/src/components/routers/RouterDetail.tsx b/webui/src/components/routers/RouterDetail.tsx
new file mode 100644
index 000000000..5f13369ab
--- /dev/null
+++ b/webui/src/components/routers/RouterDetail.tsx
@@ -0,0 +1,67 @@
+import { Flex, H1, Skeleton, Text } from '@traefiklabs/faency'
+import { useMemo } from 'react'
+import { Helmet } from 'react-helmet-async'
+
+import { DetailsCardSkeleton } from 'components/resources/DetailsCard'
+import ResourceErrors, { ResourceErrorsSkeleton } from 'components/resources/ResourceErrors'
+import RouterFlowDiagram, { RouterFlowDiagramSkeleton } from 'components/routers/RouterFlowDiagram'
+import TlsSection from 'components/routers/TlsSection'
+import { NotFound } from 'pages/NotFound'
+
+type RouterDetailProps = {
+ data?: Resource.DetailsData
+ error?: Error | null
+ name: string
+ protocol: 'http' | 'tcp' | 'udp'
+}
+
+export const RouterDetail = ({ data, error, name, protocol }: RouterDetailProps) => {
+ const isUdp = useMemo(() => protocol === 'udp', [protocol])
+
+ if (error) {
+ return (
+ <>
+
+ {name} - Traefik Proxy
+
+
+ Sorry, we could not fetch detail information for this Router right now. Please, try again later.
+
+ >
+ )
+ }
+
+ if (!data) {
+ return (
+ <>
+
+ {name} - Traefik Proxy
+
+
+
+
+
+
+
+ >
+ )
+ }
+
+ if (!data.name) {
+ return
+ }
+
+ return (
+ <>
+
+ {data.name} - Traefik Proxy
+
+ {data.name}
+
+
+ {data?.error && }
+ {!isUdp && }
+
+ >
+ )
+}
diff --git a/webui/src/components/routers/RouterFlowDiagram.tsx b/webui/src/components/routers/RouterFlowDiagram.tsx
new file mode 100644
index 000000000..870f69cd8
--- /dev/null
+++ b/webui/src/components/routers/RouterFlowDiagram.tsx
@@ -0,0 +1,212 @@
+import { Card, Flex, styled, Link, Tooltip, Box, Text, Skeleton } from '@traefiklabs/faency'
+import { useMemo } from 'react'
+import { FiArrowRight, FiGlobe, FiLayers, FiLogIn, FiZap } from 'react-icons/fi'
+
+import CopyableText from 'components/CopyableText'
+import ProviderIcon from 'components/icons/providers'
+import { ProviderName } from 'components/resources/DetailItemComponents'
+import DetailsCard, { SectionTitle } from 'components/resources/DetailsCard'
+import { ResourceStatus } from 'components/resources/ResourceStatus'
+import ScrollableCard from 'components/ScrollableCard'
+import { useHrefWithReturnTo } from 'hooks/use-href-with-return-to'
+import { useResourceDetail } from 'hooks/use-resource-detail'
+
+const FlexContainer = styled(Flex, {
+ gap: '$3',
+ flexDirection: 'column !important',
+ alignItems: 'center !important',
+ flex: '1 1 0',
+ minWidth: '0',
+ maxWidth: '100%',
+})
+
+const ArrowSeparator = () => {
+ return (
+
+
+
+ )
+}
+
+const LinkedNameAndStatus = ({ data }: { data: { status: Resource.Status; name: string; href?: string } }) => {
+ const hrefWithReturnTo = useHrefWithReturnTo(data?.href || '')
+
+ if (!data.href) {
+ return (
+
+
+
+
+
+
+
+
+ {data.name}
+
+
+ )
+ }
+ return (
+
+
+
+ {data.name}
+
+
+ )
+}
+
+type RouterFlowDiagramProps = {
+ data: Resource.DetailsData
+ protocol: 'http' | 'tcp' | 'udp'
+}
+
+const RouterFlowDiagram = ({ data, protocol }: RouterFlowDiagramProps) => {
+ const displayedEntrypoints = useMemo(() => {
+ return data?.entryPointsData?.map((point) => {
+ if (!point.message) {
+ return { key: point.name, val: point.address }
+ } else {
+ return { key: point.message, val: '' }
+ }
+ })
+ }, [data?.entryPointsData])
+
+ const routerDetailsItems = useMemo(
+ () =>
+ [
+ data.status && { key: 'Status', val: },
+ data.provider && {
+ key: 'Provider',
+ val: (
+ <>
+
+ {data.provider}
+ >
+ ),
+ },
+ data.priority && { key: 'Priority', val: data.priority },
+ data.rule && { key: 'Rule', val: },
+ ].filter(Boolean) as { key: string; val: string | React.ReactElement }[],
+ [data.priority, data.provider, data.rule, data.status],
+ )
+
+ const serviceSlug = data.service?.includes('@')
+ ? data.service
+ : `${data.service ?? 'unknown'}@${data.provider ?? 'unknown'}`
+
+ const { data: serviceData, error: serviceDataError } = useResourceDetail(serviceSlug ?? '', 'services')
+
+ return (
+
+ {!!data.using?.length && (
+ <>
+
+ } title="Entrypoints" />
+ {displayedEntrypoints?.length ? (
+
+ ) : (
+
+ )}
+
+
+
+ >
+ )}
+
+
+ } title={`${protocol.toUpperCase()} Router`} />
+
+
+
+ {data.hasValidMiddlewares && (
+ <>
+
+
+ } title={`${protocol.toUpperCase()} Middlewares`} />
+ {data.middlewares ? (
+
+
+ {data.middlewares.map((mw, idx) => {
+ const data = {
+ name: mw.name,
+ status: mw.status,
+ href: `/${protocol}/middlewares/${mw.name}`,
+ }
+ return
+ })}
+
+
+ ) : (
+
+ )}
+
+ >
+ )}
+
+
+
+
+ } title="Service" />
+
+
+
+
+
+ )
+}
+
+const DiagramCardSkeleton = () => {
+ return (
+
+ {[...Array(5)].map((_, idx) => (
+
+ ))}
+
+ )
+}
+
+export const RouterFlowDiagramSkeleton = () => {
+ return (
+
+ {[...Array(4)].map((_, index) => [
+
+
+
+ ,
+ index < 3 && ,
+ ])}
+
+ )
+}
+
+export default RouterFlowDiagram
diff --git a/webui/src/components/routers/TlsIcon.tsx b/webui/src/components/routers/TlsIcon.tsx
new file mode 100644
index 000000000..b6a93bede
--- /dev/null
+++ b/webui/src/components/routers/TlsIcon.tsx
@@ -0,0 +1,7 @@
+import { FiShield } from 'react-icons/fi'
+
+const TlsIcon = ({ size = 20 }: { size?: number }) => {
+ return
+}
+
+export default TlsIcon
diff --git a/webui/src/components/routers/TlsSection.tsx b/webui/src/components/routers/TlsSection.tsx
new file mode 100644
index 000000000..7938b87a6
--- /dev/null
+++ b/webui/src/components/routers/TlsSection.tsx
@@ -0,0 +1,78 @@
+import { Badge, Box, Card, Flex } from '@traefiklabs/faency'
+import { useMemo } from 'react'
+
+import TlsIcon from './TlsIcon'
+
+import { EmptyIcon } from 'components/icons/EmptyIcon'
+import { BooleanState, EmptyPlaceholder } from 'components/resources/DetailItemComponents'
+import DetailsCard, { SectionTitle } from 'components/resources/DetailsCard'
+
+type Props = {
+ data?: Router.TLS
+}
+
+const TlsSection = ({ data }: Props) => {
+ const items = useMemo(() => {
+ if (data) {
+ return [
+ data?.options && { key: 'Options', val: data.options },
+ { key: 'Passthrough', val: },
+ data?.certResolver && { key: 'Certificate resolver', val: data.certResolver },
+ data?.domains && {
+ stackVertical: true,
+ forceNewRow: true,
+ key: 'Domains',
+ val: (
+
+ {data.domains?.map((domain) => (
+
+
+
+ {domain.main}
+
+
+ {domain.sans?.map((sub) => (
+
+ {sub}
+
+ ))}
+
+ ))}
+
+ ),
+ },
+ ].filter(Boolean) as { key: string; val: string | React.ReactElement }[]
+ }
+ }, [data])
+ return (
+
+ } title="TLS" />
+ {items?.length ? (
+
+ ) : (
+
+
+
+
+
+
+ There is no
+
+ TLS configured
+
+
+
+ )}
+
+ )
+}
+
+export default TlsSection
diff --git a/webui/src/components/services/MirrorServices.tsx b/webui/src/components/services/MirrorServices.tsx
new file mode 100644
index 000000000..b551afbc7
--- /dev/null
+++ b/webui/src/components/services/MirrorServices.tsx
@@ -0,0 +1,42 @@
+import { Flex, Text } from '@traefiklabs/faency'
+import { FiGlobe } from 'react-icons/fi'
+
+import { getProviderFromName } from './Servers'
+
+import ProviderIcon from 'components/icons/providers'
+import { SectionTitle } from 'components/resources/DetailsCard'
+import PaginatedTable from 'components/tables/PaginatedTable'
+
+type MirrorServicesProps = {
+ mirrors: Service.Mirror[]
+ defaultProvider: string
+}
+
+const MirrorServices = ({ mirrors, defaultProvider }: MirrorServicesProps) => {
+ return (
+
+ } title="Mirror Services" />
+ ({
+ name: mirror.name,
+ percent: mirror.percent,
+ provider: getProviderFromName(mirror.name, defaultProvider),
+ }))}
+ columns={[
+ { key: 'name', header: 'Name' },
+ { key: 'percent', header: 'Percent' },
+ { key: 'provider', header: 'Provider' },
+ ]}
+ testId="mirror-services"
+ renderCell={(key, value) => {
+ if (key === 'provider') {
+ return
+ }
+ return {value}
+ }}
+ />
+
+ )
+}
+
+export default MirrorServices
diff --git a/webui/src/components/services/Servers.tsx b/webui/src/components/services/Servers.tsx
new file mode 100644
index 000000000..684a7d143
--- /dev/null
+++ b/webui/src/components/services/Servers.tsx
@@ -0,0 +1,93 @@
+import { Flex, Text } from '@traefiklabs/faency'
+import { useMemo } from 'react'
+import { FiGlobe } from 'react-icons/fi'
+
+import { SectionTitle } from 'components/resources/DetailsCard'
+import { ResourceStatus } from 'components/resources/ResourceStatus'
+import { colorByStatus } from 'components/resources/Status'
+import PaginatedTable from 'components/tables/PaginatedTable'
+import Tooltip from 'components/Tooltip'
+
+type ServersProps = {
+ data: Service.Details
+ protocol: 'http' | 'tcp' | 'udp'
+}
+
+type Server = {
+ url?: string
+ address?: string
+}
+
+type ServerStatus = {
+ [server: string]: string
+}
+
+function getServerStatusList(data: Service.Details): ServerStatus {
+ const serversList: ServerStatus = {}
+
+ data.loadBalancer?.servers?.forEach((server: Server) => {
+ const serverKey = server.address || server.url
+ if (serverKey) {
+ serversList[serverKey] = 'DOWN'
+ }
+ })
+
+ if (data.serverStatus) {
+ Object.entries(data.serverStatus).forEach(([server, status]) => {
+ serversList[server] = status
+ })
+ }
+
+ return serversList
+}
+
+export const getProviderFromName = (serviceName: string, defaultProvider: string): string => {
+ const [, provider] = serviceName.split('@')
+ return provider || defaultProvider
+}
+
+const Servers = ({ data, protocol }: ServersProps) => {
+ const serversList = getServerStatusList(data)
+
+ const isTcp = useMemo(() => protocol === 'tcp', [protocol])
+ const isUdp = useMemo(() => protocol === 'udp', [protocol])
+
+ if (!Object.keys(serversList)?.length) return null
+
+ return (
+
+ } title="Servers" />
+ ({
+ server,
+ status,
+ }))}
+ columns={[
+ ...(isUdp ? [] : [{ key: 'status' as const, header: 'Status' }]),
+ { key: 'server' as const, header: isTcp ? 'Address' : 'URL' },
+ ]}
+ testId="servers-list"
+ renderCell={(key, value) => {
+ if (key === 'status') {
+ return (
+
+
+ {value}
+
+ )
+ }
+ if (key === 'server') {
+ return (
+
+ {value}
+
+ )
+ }
+ return {value}
+ }}
+ />
+
+ )
+}
+
+export default Servers
diff --git a/webui/src/components/services/ServiceDefinition.tsx b/webui/src/components/services/ServiceDefinition.tsx
new file mode 100644
index 000000000..3cd3b16c5
--- /dev/null
+++ b/webui/src/components/services/ServiceDefinition.tsx
@@ -0,0 +1,58 @@
+import { Badge } from '@traefiklabs/faency'
+import { useMemo } from 'react'
+
+import ProviderIcon from 'components/icons/providers'
+import { BooleanState, ProviderName } from 'components/resources/DetailItemComponents'
+import DetailsCard from 'components/resources/DetailsCard'
+import { ResourceStatus } from 'components/resources/ResourceStatus'
+
+type ServiceDefinitionProps = {
+ data: Service.Details
+ testId?: string
+}
+
+const ServiceDefinition = ({ data, testId }: ServiceDefinitionProps) => {
+ const providerName = useMemo(() => {
+ return data.provider
+ }, [data.provider])
+
+ const detailsItems = useMemo(
+ () =>
+ [
+ data.status && { key: 'Status', val: },
+ data.type && { key: 'Type', val: data.type },
+ data.provider && {
+ key: 'Provider',
+ val: (
+ <>
+
+ {providerName}
+ >
+ ),
+ },
+ data.mirroring &&
+ data.mirroring.service && { key: 'Main service', val: {data.mirroring.service} },
+ data.loadBalancer?.passHostHeader && {
+ key: 'Pass host header',
+ val: ,
+ },
+ data.loadBalancer?.terminationDelay && {
+ key: 'Termination delay',
+ val: `${data.loadBalancer.terminationDelay} ms`,
+ },
+ ].filter(Boolean) as { key: string; val: string | React.ReactElement }[],
+ [
+ data.loadBalancer?.passHostHeader,
+ data.loadBalancer?.terminationDelay,
+ data.mirroring,
+ data.provider,
+ data.status,
+ data.type,
+ providerName,
+ ],
+ )
+
+ return
+}
+
+export default ServiceDefinition
diff --git a/webui/src/components/services/ServiceDetail.tsx b/webui/src/components/services/ServiceDetail.tsx
new file mode 100644
index 000000000..86adf8b26
--- /dev/null
+++ b/webui/src/components/services/ServiceDetail.tsx
@@ -0,0 +1,87 @@
+import { Box, Flex, H1, Skeleton, Text } from '@traefiklabs/faency'
+import { Helmet } from 'react-helmet-async'
+
+import MirrorServices from './MirrorServices'
+import Servers from './Servers'
+import ServiceDefinition from './ServiceDefinition'
+import ServiceHealthCheck from './ServiceHealthCheck'
+import WeightedServices from './WeightedServices'
+
+import { DetailsCardSkeleton } from 'components/resources/DetailsCard'
+import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection'
+import AriaTableSkeleton from 'components/tables/AriaTableSkeleton'
+import { NotFound } from 'pages/NotFound'
+
+type ServiceDetailProps = {
+ data?: Resource.DetailsData
+ error?: Error
+ name: string
+ protocol: 'http' | 'tcp' | 'udp'
+}
+
+export const ServiceDetail = ({ data, error, name, protocol }: ServiceDetailProps) => {
+ if (error) {
+ return (
+ <>
+
+ {name} - Traefik Proxy
+
+
+ Sorry, we could not fetch detail information for this Service right now. Please, try again later.
+
+ >
+ )
+ }
+
+ if (!data) {
+ return (
+ <>
+
+ {name} - Traefik Proxy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+ }
+
+ if (!data.name) {
+ return
+ }
+
+ return (
+ <>
+
+ {data.name} - Traefik Proxy
+
+ {data.name}
+
+
+
+ {data.loadBalancer?.healthCheck && }
+ {!!data?.weighted?.services?.length && (
+
+ )}
+
+ {!!data?.mirroring?.mirrors && (
+
+ )}
+
+
+ >
+ )
+}
diff --git a/webui/src/components/services/ServiceHealthCheck.tsx b/webui/src/components/services/ServiceHealthCheck.tsx
new file mode 100644
index 000000000..c9d77f29a
--- /dev/null
+++ b/webui/src/components/services/ServiceHealthCheck.tsx
@@ -0,0 +1,66 @@
+import { useMemo } from 'react'
+import { FiShield } from 'react-icons/fi'
+
+import CopyableText from 'components/CopyableText'
+import { Chips } from 'components/resources/DetailItemComponents'
+import DetailsCard from 'components/resources/DetailsCard'
+
+type ServiceHealthCheckProps = {
+ data: Service.Details
+ protocol: 'http' | 'tcp' | 'udp'
+}
+
+const ServiceHealthCheck = ({ data, protocol }: ServiceHealthCheckProps) => {
+ const isTcp = useMemo(() => protocol === 'tcp', [protocol])
+
+ const healthCheckItems = useMemo(() => {
+ if (data.loadBalancer?.healthCheck) {
+ const healthCheck = data.loadBalancer.healthCheck
+ if (isTcp) {
+ return [
+ healthCheck?.interval && { key: 'Interval', val: healthCheck.interval },
+ healthCheck?.timeout && { key: 'Timeout', val: healthCheck.timeout },
+ healthCheck?.port && { key: 'Port', val: healthCheck.port },
+ healthCheck?.unhealthyInterval && { key: 'Unhealthy interval', val: healthCheck.unhealthyInterval },
+ healthCheck?.send && {
+ key: 'Send',
+ val: ,
+ },
+ healthCheck?.expect && {
+ key: 'Expect',
+ val: ,
+ },
+ ].filter(Boolean) as { key: string; val: string | React.ReactElement; stackVertical?: boolean }[]
+ } else {
+ return [
+ healthCheck?.scheme && { key: 'Scheme', val: healthCheck.scheme },
+ healthCheck?.interval && { key: 'Interval', val: healthCheck.interval },
+ healthCheck?.path && {
+ key: 'Path',
+ val: ,
+ },
+ healthCheck?.timeout && { key: 'Timeout', val: healthCheck.timeout },
+ healthCheck?.port && { key: 'Port', val: String(healthCheck.port) },
+ healthCheck?.hostname && {
+ key: 'Hostname',
+ val: ,
+ },
+ healthCheck.headers && {
+ key: 'Headers',
+ val: entry.join(': '))} />,
+ stackVertical: true,
+ forceNewRow: true,
+ },
+ ].filter(Boolean) as { key: string; val: string | React.ReactElement; stackVertical?: boolean }[]
+ }
+ }
+ }, [data.loadBalancer?.healthCheck, isTcp])
+
+ if (!healthCheckItems) return null
+
+ return (
+ } title="Health Check" items={healthCheckItems} testId="health-check" />
+ )
+}
+
+export default ServiceHealthCheck
diff --git a/webui/src/components/services/WeightedServices.tsx b/webui/src/components/services/WeightedServices.tsx
new file mode 100644
index 000000000..6ddae63b1
--- /dev/null
+++ b/webui/src/components/services/WeightedServices.tsx
@@ -0,0 +1,42 @@
+import { Flex } from '@traefiklabs/faency'
+import { FiGlobe } from 'react-icons/fi'
+
+import { getProviderFromName } from './utils'
+
+import ProviderIcon from 'components/icons/providers'
+import { SectionTitle } from 'components/resources/DetailsCard'
+import PaginatedTable from 'components/tables/PaginatedTable'
+
+type WeightedServicesProps = {
+ services: Service.WeightedService[]
+ defaultProvider: string
+}
+
+const WeightedServices = ({ services, defaultProvider }: WeightedServicesProps) => {
+ return (
+
+ } title="Services" />
+ ({
+ name: service.name,
+ weight: service.weight,
+ provider: getProviderFromName(service.name, defaultProvider),
+ }))}
+ columns={[
+ { key: 'name', header: 'Name' },
+ { key: 'weight', header: 'Weight' },
+ { key: 'provider', header: 'Provider' },
+ ]}
+ testId="weighted-services"
+ renderCell={(key, value) => {
+ if (key === 'provider') {
+ return
+ }
+ return value
+ }}
+ />
+
+ )
+}
+
+export default WeightedServices
diff --git a/webui/src/components/services/utils.ts b/webui/src/components/services/utils.ts
new file mode 100644
index 000000000..3dfec9acb
--- /dev/null
+++ b/webui/src/components/services/utils.ts
@@ -0,0 +1,4 @@
+export const getProviderFromName = (serviceName: string, defaultProvider: string): string => {
+ const [, provider] = serviceName.split('@')
+ return provider || defaultProvider
+}
diff --git a/webui/src/components/tables/AriaTableSkeleton.tsx b/webui/src/components/tables/AriaTableSkeleton.tsx
new file mode 100644
index 000000000..f9f30d8ed
--- /dev/null
+++ b/webui/src/components/tables/AriaTableSkeleton.tsx
@@ -0,0 +1,79 @@
+import {
+ AriaTable,
+ AriaTbody,
+ AriaTr,
+ CSS,
+ AriaTd,
+ Flex,
+ Skeleton as FaencySkeleton,
+ VariantProps,
+ AriaThead,
+ AriaTh,
+} from '@traefiklabs/faency'
+import { ReactNode } from 'react'
+
+type AriaTableSkeletonProps = {
+ children?: ReactNode
+ columns?: number
+ css?: CSS
+ lastColumnIsNarrow?: boolean
+ rowHeight?: string
+ rows?: number
+ skeletonWidth?: string
+}
+
+interface AriaTdSkeletonProps extends VariantProps {
+ css?: CSS
+ flexCss?: CSS
+}
+const AriaTdSkeleton = ({ css = {}, flexCss = {} }: AriaTdSkeletonProps) => (
+
+
+
+
+
+)
+
+const AriaThSkeleton = ({ css = {}, flexCss = {} }: AriaTdSkeletonProps) => (
+
+
+
+
+
+)
+
+export default function AriaTableSkeleton({
+ columns = 3,
+ css,
+ lastColumnIsNarrow = false,
+ rowHeight = undefined,
+ rows = 5,
+ skeletonWidth = '50%',
+}: AriaTableSkeletonProps) {
+ return (
+
+
+
+ {[...Array(columns)].map((_, colIdx) => (
+
+ ))}
+
+
+
+ {[...Array(rows)].map((_, rowIdx) => (
+
+ {[...Array(columns)].map((_, colIdx) => (
+
+ ))}
+
+ ))}
+
+
+ )
+}
diff --git a/webui/src/components/ClickableRow.tsx b/webui/src/components/tables/ClickableRow.tsx
similarity index 100%
rename from webui/src/components/ClickableRow.tsx
rename to webui/src/components/tables/ClickableRow.tsx
diff --git a/webui/src/components/tables/PaginatedTable.tsx b/webui/src/components/tables/PaginatedTable.tsx
new file mode 100644
index 000000000..2d403dfd8
--- /dev/null
+++ b/webui/src/components/tables/PaginatedTable.tsx
@@ -0,0 +1,153 @@
+import { AriaTable, AriaTbody, AriaTd, AriaThead, AriaTr, Box, Button, Flex, Text } from '@traefiklabs/faency'
+import { ReactNode, useEffect, useRef, useState } from 'react'
+import { FiChevronLeft, FiChevronRight, FiChevronsLeft, FiChevronsRight } from 'react-icons/fi'
+
+import SortableTh from './SortableTh'
+
+type PaginatedTableProps> = {
+ data: T[]
+ columns: {
+ key: keyof T
+ header: string
+ sortable?: boolean
+ width?: string
+ }[]
+ itemsPerPage?: number
+ testId?: string
+ renderCell?: (key: keyof T, value: T[keyof T], row: T) => ReactNode
+ renderRow?: (row: T) => ReactNode
+}
+
+const PaginatedTable = >({
+ data,
+ columns,
+ itemsPerPage = 5,
+ testId,
+ renderCell,
+ renderRow,
+}: PaginatedTableProps) => {
+ const [currentPage, setCurrentPage] = useState(0)
+ const [tableHeight, setTableHeight] = useState(undefined)
+ const tableRef = useRef(null)
+
+ const totalPages = Math.ceil(data.length / itemsPerPage)
+ const startIndex = currentPage * itemsPerPage
+ const endIndex = startIndex + itemsPerPage
+ const currentData = data.slice(startIndex, endIndex)
+
+ // Workaround to keep the same height to avoid layout shift
+ useEffect(() => {
+ if (totalPages > 1 && currentPage === 0 && tableRef.current && !tableHeight) {
+ const height = tableRef.current.offsetHeight
+ setTableHeight(height)
+ }
+ }, [totalPages, currentPage, tableHeight])
+
+ const handleFirstPage = () => {
+ setCurrentPage(0)
+ }
+
+ const handlePreviousPage = () => {
+ setCurrentPage((prev) => Math.max(0, prev - 1))
+ }
+
+ const handleNextPage = () => {
+ setCurrentPage((prev) => Math.min(totalPages - 1, prev + 1))
+ }
+
+ const handleLastPage = () => {
+ setCurrentPage(totalPages - 1)
+ }
+
+ const getCellContent = (key: keyof T, value: T[keyof T], row: T) => {
+ if (renderCell) {
+ return renderCell(key, value, row)
+ }
+ return value as ReactNode
+ }
+
+ return (
+
+ 1 && tableHeight ? { minHeight: `${tableHeight}px` } : undefined}>
+
+
+ {columns.map((column) => (
+
+ ))}
+
+
+
+ 1 && tableHeight ? { verticalAlign: 'top' } : undefined}>
+ {currentData.map((row, rowIndex) => {
+ if (renderRow) {
+ return renderRow(row)
+ }
+
+ const rowContent = (
+ <>
+ {columns?.map((column) => (
+ {getCellContent(column.key, row[column.key], row)}
+ ))}
+ >
+ )
+
+ return {rowContent}
+ })}
+
+
+ {totalPages > 1 && (
+
+
+
+
+
+
+ Page {currentPage + 1} of {totalPages}
+
+
+
+
+ )}
+
+ )
+}
+
+export default PaginatedTable
diff --git a/webui/src/components/TableFilter.tsx b/webui/src/components/tables/TableFilter.tsx
similarity index 100%
rename from webui/src/components/TableFilter.tsx
rename to webui/src/components/tables/TableFilter.tsx
diff --git a/webui/src/hooks/use-href-with-return-to.ts b/webui/src/hooks/use-href-with-return-to.ts
index cbf938f7e..8a993e500 100644
--- a/webui/src/hooks/use-href-with-return-to.ts
+++ b/webui/src/hooks/use-href-with-return-to.ts
@@ -39,7 +39,7 @@ const RETURN_TO_LABEL_OVERRIDES_SINGULAR: Record>
},
udp: {
routers: 'UDP router',
- services: 'TCP service',
+ services: 'UDP service',
},
}
@@ -56,7 +56,7 @@ const RETURN_TO_LABEL_OVERRIDES_PLURAL: Record> =
},
udp: {
routers: 'UDP routers',
- services: 'TCP services',
+ services: 'UDP services',
},
}
diff --git a/webui/src/hooks/use-resource-detail.tsx b/webui/src/hooks/use-resource-detail.tsx
index df6e38941..7a3c7ba09 100644
--- a/webui/src/hooks/use-resource-detail.tsx
+++ b/webui/src/hooks/use-resource-detail.tsx
@@ -2,115 +2,8 @@ import useSWR from 'swr'
import fetchMany from 'libs/fetchMany'
-export type EntryPoint = {
- name: string
- address: string
- message?: string
-}
-
-type JSONObject = {
- [x: string]: string | number
-}
-export type ValuesMapType = {
- [key: string]: string | number | JSONObject
-}
-
-export type MiddlewareProps = {
- [prop: string]: ValuesMapType
-}
-
-export type Middleware = {
- name: string
- status: 'enabled' | 'disabled' | 'warning'
- provider: string
- type?: string
- plugin?: Record
- error?: string[]
- routers?: string[]
- usedBy?: string[]
-} & MiddlewareProps
-
-type Router = {
- name: string
- service?: string
- status: 'enabled' | 'disabled' | 'warning'
- rule?: string
- priority?: number
- provider: string
- tls?: {
- options: string
- certResolver: string
- domains: TlsDomain[]
- passthrough: boolean
- }
- error?: string[]
- entryPoints?: string[]
- message?: string
-}
-
-type TlsDomain = {
- main: string
- sans: string[]
-}
-
-export type RouterDetailType = Router & {
- middlewares?: Middleware[]
- hasValidMiddlewares?: boolean
- entryPointsData?: EntryPoint[]
- using?: string[]
-}
-
-type Mirror = {
- name: string
- percent: number
-}
-
-export type ServiceDetailType = {
- name: string
- status: 'enabled' | 'disabled' | 'warning'
- provider: string
- type: string
- usedBy?: string[]
- routers?: Router[]
- serverStatus?: {
- [server: string]: string
- }
- mirroring?: {
- service: string
- mirrors?: Mirror[]
- }
- loadBalancer?: {
- servers?: { url: string }[]
- passHostHeader?: boolean
- terminationDelay?: number
- healthCheck?: {
- scheme: string
- path: string
- port: number
- interval: string
- timeout: string
- hostname: string
- headers?: {
- [header: string]: string
- }
- }
- }
- weighted?: {
- services?: {
- name: string
- weight: number
- }[]
- }
-}
-
-export type MiddlewareDetailType = Middleware & {
- routers?: Router[]
-}
-
-export type ResourceDetailDataType = RouterDetailType & ServiceDetailType & MiddlewareDetailType
-
type ResourceDetailType = {
- data?: ResourceDetailDataType
+ data?: Resource.DetailsData
error?: Error
}
@@ -128,7 +21,7 @@ export const useResourceDetail = (name: string, resource: string, protocol = 'ht
}
const firstError = error || entryPointsError || middlewaresError || routersError
- const validMiddlewares = (middlewares as Middleware[] | undefined)?.filter((mw) => !!mw.name)
+ const validMiddlewares = (middlewares as Middleware.Details[] | undefined)?.filter((mw) => !!mw.name)
const hasMiddlewares = validMiddlewares
? validMiddlewares.length > 0
: routeDetail.middlewares && routeDetail.middlewares.length > 0
diff --git a/webui/src/layout/navigation/TopNavBar.tsx b/webui/src/layout/navigation/TopNavBar.tsx
index e31092098..902573d0f 100644
--- a/webui/src/layout/navigation/TopNavBar.tsx
+++ b/webui/src/layout/navigation/TopNavBar.tsx
@@ -13,7 +13,8 @@ import {
Text,
Tooltip,
} from '@traefiklabs/faency'
-import { useContext, useEffect, useMemo, useState } from 'react'
+import { useContext, useMemo } from 'react'
+import { Helmet } from 'react-helmet-async'
import { FiBookOpen, FiChevronLeft, FiGithub, FiHeart, FiHelpCircle } from 'react-icons/fi'
import { useLocation } from 'react-router-dom'
@@ -22,6 +23,7 @@ import { DARK_PRIMARY_COLOR, LIGHT_PRIMARY_COLOR } from '../Page'
import ThemeSwitcher from 'components/ThemeSwitcher'
import { VersionContext } from 'contexts/version'
import { useRouterReturnTo } from 'hooks/use-href-with-return-to'
+import useHubUpgradeButton from 'hooks/use-hub-upgrade-button'
import { useIsDarkMode } from 'hooks/use-theme'
const TopNavBarBackLink = () => {
@@ -43,8 +45,7 @@ const TopNavBarBackLink = () => {
}
export const TopNav = ({ css, noHubButton = false }: { css?: CSS; noHubButton?: boolean }) => {
- const [hasHubButtonComponent, setHasHubButtonComponent] = useState(false)
- const { showHubButton, version } = useContext(VersionContext)
+ const { version } = useContext(VersionContext)
const isDarkMode = useIsDarkMode()
const parsedVersion = useMemo(() => {
@@ -58,101 +59,86 @@ export const TopNav = ({ css, noHubButton = false }: { css?: CSS; noHubButton?:
return matches ? 'v' + matches[1] : 'master'
}, [version])
- useEffect(() => {
- if (!showHubButton) {
- setHasHubButtonComponent(false)
- return
- }
+ const { signatureVerified, scriptBlobUrl, isCustomElementDefined } = useHubUpgradeButton()
- if (customElements.get('hub-button-app')) {
- setHasHubButtonComponent(true)
- return
- }
-
- const scripts: HTMLScriptElement[] = []
- const createScript = (scriptSrc: string): HTMLScriptElement => {
- const script = document.createElement('script')
- script.src = scriptSrc
- script.async = true
- script.onload = () => {
- setHasHubButtonComponent(customElements.get('hub-button-app') !== undefined)
- }
- scripts.push(script)
- return script
- }
-
- // Source: https://github.com/traefik/traefiklabs-hub-button-app
- document.head.appendChild(createScript('traefiklabs-hub-button-app/main-v1.js'))
-
- return () => {
- // Remove the scripts on unmount.
- scripts.forEach((script) => {
- if (script.parentNode) {
- script.parentNode.removeChild(script)
- }
- })
- }
- }, [showHubButton])
+ const displayUpgradeToHubButton = useMemo(
+ () => !noHubButton && signatureVerified && (!!scriptBlobUrl || isCustomElementDefined),
+ [isCustomElementDefined, noHubButton, scriptBlobUrl, signatureVerified],
+ )
return (
-
-
-
- {!noHubButton && hasHubButtonComponent && (
-
-
-
- )}
-
-
-
-
-
-
+ <>
+ {displayUpgradeToHubButton && (
+
+
+
+
+ )}
+
+
+
+ {displayUpgradeToHubButton && (
+
+
+
+ )}
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- Documentation
-
-
-
-
-
-
-
- Github Repository
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ Documentation
+
+
+
+
+
+
+
+ Github Repository
+
+
+
+
+
+
+
+
-
+ >
)
}
diff --git a/webui/src/libs/objectHandlers.ts b/webui/src/libs/objectHandlers.ts
deleted file mode 100644
index 59c3bcf0c..000000000
--- a/webui/src/libs/objectHandlers.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-type ObjectWithMessage = {
- message?: string
-}
-
-export const getValidData = (data?: T[]): T[] =>
- data ? data.filter((item) => !item.message) : []
-export const getErrorData = (data?: T[]): T[] =>
- data ? data.filter((item) => !!item.message) : []
diff --git a/webui/src/libs/parsers.ts b/webui/src/libs/parsers.ts
index 26f2ed107..46922f217 100644
--- a/webui/src/libs/parsers.ts
+++ b/webui/src/libs/parsers.ts
@@ -1,6 +1,4 @@
-import { Middleware } from 'hooks/use-resource-detail'
-
-export const parseMiddlewareType = (middleware: Middleware): string | undefined => {
+export const parseMiddlewareType = (middleware: Middleware.Props): string | undefined => {
if (middleware.plugin) {
const pluginObject = middleware.plugin || {}
const [pluginName] = Object.keys(pluginObject)
diff --git a/webui/src/mocks/data/api-entrypoints.json b/webui/src/mocks/data/api-entrypoints.json
index 456477530..b91f638ca 100644
--- a/webui/src/mocks/data/api-entrypoints.json
+++ b/webui/src/mocks/data/api-entrypoints.json
@@ -63,6 +63,19 @@
},
"forwardedHeaders": {},
"name": "web-secured"
+ },
+ {
+ "address": ":443",
+ "transport": {
+ "lifeCycle": {
+ "graceTimeOut": 10000000000
+ },
+ "respondingTimeouts": {
+ "idleTimeout": 180000000000
+ }
+ },
+ "forwardedHeaders": {},
+ "name": "web-secured-longer-name"
},
{
"address": ":8100",
diff --git a/webui/src/mocks/data/api-http_middlewares.json b/webui/src/mocks/data/api-http_middlewares.json
index a8737d28e..23433f20c 100644
--- a/webui/src/mocks/data/api-http_middlewares.json
+++ b/webui/src/mocks/data/api-http_middlewares.json
@@ -1,13 +1,24 @@
[
{
- "addPrefix": {
- "prefix": "/foo"
- },
"status": "enabled",
"usedBy": ["web@docker"],
"name": "add-foo@docker",
"type": "addprefix",
- "provider": "docker"
+ "provider": "docker",
+ "addPrefix": {
+ "prefix": "/path",
+ "aCustomObject": {
+ "array of arrays": [
+ [1, 2],
+ [3, 4]
+ ],
+ "array of objects": [{ "some": "value" }, { "another": "value" }],
+ "array of booleans": [true, false, true],
+ "array of numbers": [10, 100, 1000],
+ "array of strings": ["value1", "value2"]
+ }
+ },
+ "error": ["message 1", "message 2"]
},
{
"redirectScheme": {
@@ -55,14 +66,21 @@
"addPrefix": {
"prefix": "/path",
"aCustomObject": {
- "array of arrays": [[1, 2], [3, 4]],
+ "array of arrays": [
+ [1, 2],
+ [3, 4]
+ ],
"array of objects": [{ "some": "value" }, { "another": "value" }],
"array of booleans": [true, false, true],
"array of numbers": [10, 100, 1000],
"array of strings": ["value1", "value2"]
}
},
- "error": ["message 1", "message 2"],
+ "error": [
+ "message 1",
+ "message 2",
+ "('colorByStatus' export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports"
+ ],
"status": "enabled",
"usedBy": ["foo@docker", "bar@file"],
"name": "middleware00@docker",
@@ -144,7 +162,10 @@
},
{
"basicAuth": {
- "users": ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP4HxwgUir3HP4EsggP/QNo0"],
+ "users": [
+ "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
+ "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP4HxwgUir3HP4EsggP/QNo0"
+ ],
"usersFile": "/etc/foo/my/file/path/.htpasswd",
"realm": "Hello you are here",
"removeHeader": true,
diff --git a/webui/src/mocks/data/api-http_routers.json b/webui/src/mocks/data/api-http_routers.json
index 227aa7cc3..5e81954de 100644
--- a/webui/src/mocks/data/api-http_routers.json
+++ b/webui/src/mocks/data/api-http_routers.json
@@ -4,10 +4,7 @@
"rule": "Host(`jaeger-v2-example-beta1`)",
"status": "enabled",
"name": "jaeger_v2-example-beta1@docker",
- "using": [
- "web-secured",
- "web"
- ],
+ "using": ["web-secured", "web"],
"priority": 10,
"provider": "docker"
},
@@ -20,6 +17,24 @@
],
"status": "disabled",
"name": "orphan-router@file",
+ "tls": {
+ "options": "foo@file",
+ "certResolver": "acme-dns-challenge",
+ "domains": [
+ {
+ "main": "example.com",
+ "sans": ["foo.example.com", "bar.example.com"]
+ },
+ {
+ "main": "domain.com",
+ "sans": ["foo.domain.com", "bar.domain.com"]
+ },
+ {
+ "main": "my.domain.com",
+ "sans": ["foo.my.domain.com", "bar.my.domain.com"]
+ }
+ ]
+ },
"middlewares": [
"middleware00@docker",
"middleware01@docker",
@@ -43,20 +58,12 @@
"middleware19@docker",
"middleware20@docker"
],
- "using": [
- "web-secured",
- "web",
- "traefik",
- "web2",
- "web3"
- ],
+ "using": ["web-secured", "web", "traefik", "web2", "web3"],
"priority": 30,
"provider": "file"
},
{
- "entryPoints": [
- "web-mtls"
- ],
+ "entryPoints": ["web-mtls"],
"service": "api3_v2-example-beta1",
"rule": "Host(`server`) \u0026\u0026 Path(`/mtls`)",
"tls": {
@@ -65,24 +72,15 @@
"domains": [
{
"main": "example.com",
- "sans": [
- "foo.example.com",
- "bar.example.com"
- ]
+ "sans": ["foo.example.com", "bar.example.com"]
},
{
"main": "domain.com",
- "sans": [
- "foo.domain.com",
- "bar.domain.com"
- ]
+ "sans": ["foo.domain.com", "bar.domain.com"]
},
{
"main": "my.domain.com",
- "sans": [
- "foo.my.domain.com",
- "bar.my.domain.com"
- ]
+ "sans": ["foo.my.domain.com", "bar.my.domain.com"]
}
]
},
@@ -90,39 +88,27 @@
"priority": 42,
"name": "server-mtls@docker",
"provider": "docker",
- "using": [
- "web-mtls"
- ]
+ "using": ["web-mtls"]
},
{
- "entryPoints": [
- "web-redirect"
- ],
- "middlewares": [
- "redirect@file"
- ],
+ "entryPoints": ["web-redirect"],
+ "middlewares": ["redirect@file"],
"service": "api2_v2-example-beta1",
"rule": "Host(`server`)",
"status": "enabled",
"name": "server-redirect@docker",
- "using": [
- "web-redirect"
- ],
+ "using": ["web-redirect"],
"priority": 9223372036854776000,
"provider": "docker"
},
{
- "entryPoints": [
- "web-secured"
- ],
+ "entryPoints": ["web-secured"],
"service": "api2_v2-example-beta1",
"rule": "Host(`server`)",
"tls": {},
"status": "enabled",
"name": "server-secured@docker",
- "using": [
- "web-secured"
- ],
+ "using": ["web-secured"],
"provider": "docker"
},
{
@@ -130,42 +116,27 @@
"rule": "Host(`traefik-v2-example-beta1`)",
"status": "enabled",
"name": "traefik_v2-example-beta1@docker",
- "using": [
- "web-secured",
- "web"
- ],
+ "using": ["web-secured", "web"],
"provider": "docker"
},
{
- "entryPoints": [
- "web"
- ],
- "middlewares": [
- "add-foo"
- ],
+ "entryPoints": ["web"],
+ "middlewares": ["add-foo"],
"service": "api_v2-example-beta1",
"rule": "Host(`jorge.dockeree.containous.cloud`)",
"status": "enabled",
"name": "web@docker",
- "using": [
- "web"
- ],
+ "using": ["web"],
"provider": "docker"
},
{
- "entryPoints": [
- "web"
- ],
- "middlewares": [
- "whoami-app-hello-tls-jwt-ef36e528ebdc93bc4f2a-service-middleware"
- ],
+ "entryPoints": ["web"],
+ "middlewares": ["whoami-app-hello-tls-jwt-ef36e528ebdc93bc4f2a-service-middleware"],
"service": "whoami-app-hello-tls-jwt-ef36e528ebdc93bc4f2a-service",
"rule": "Host(`jorge.dockeree.containous.cloud`)",
"status": "enabled",
"name": "whoami-app-hello-tls-jwt-ef36e528ebdc93bc4f2a@kubernetescrd",
- "using": [
- "web"
- ],
+ "using": ["web"],
"provider": "docker"
}
]
diff --git a/webui/src/mocks/data/api-http_services.json b/webui/src/mocks/data/api-http_services.json
index 41edc7076..6a139ed17 100644
--- a/webui/src/mocks/data/api-http_services.json
+++ b/webui/src/mocks/data/api-http_services.json
@@ -9,10 +9,7 @@
"passHostHeader": true
},
"status": "enabled",
- "usedBy": [
- "server-redirect@docker",
- "server-secured@docker"
- ],
+ "usedBy": ["server-redirect@docker", "server-secured@docker"],
"serverStatus": {
"http://10.0.1.12:80": "UP"
},
@@ -95,9 +92,7 @@
}
},
"status": "enabled",
- "usedBy": [
- "server-mtls@docker"
- ],
+ "usedBy": ["server-mtls@docker"],
"serverStatus": {
"http://10.0.1.20:80": "UP",
"http://10.0.1.21:80": "UP",
@@ -107,8 +102,25 @@
"http://10.0.1.25:80": "UP"
},
"name": "api3_v2-example-beta1@docker",
- "type": "loadbalancer",
- "provider": "docker"
+ "type": "mirroring",
+ "provider": "docker",
+ "mirroring": {
+ "mirrors": [
+ {
+ "name": "two@docker",
+ "percent": 10
+ },
+ {
+ "name": "three@docker",
+ "percent": 15
+ },
+ {
+ "name": "four@docker",
+ "percent": 80
+ }
+ ],
+ "service": "one@docker"
+ }
},
{
"loadBalancer": {
@@ -120,9 +132,7 @@
"passHostHeader": true
},
"status": "enabled",
- "usedBy": [
- "web@docker"
- ],
+ "usedBy": ["web@docker"],
"serverStatus": {
"http://10.0.1.11:80": "UP"
},
@@ -140,9 +150,7 @@
"passHostHeader": true
},
"status": "enabled",
- "usedBy": [
- "jaeger_v2-example-beta1@docker"
- ],
+ "usedBy": ["jaeger_v2-example-beta1@docker"],
"serverStatus": {
"http://10.0.1.20:5775": "UP"
},
@@ -174,9 +182,7 @@
"passHostHeader": true
},
"status": "enabled",
- "usedBy": [
- "traefik_v2-example-beta1@docker"
- ],
+ "usedBy": ["traefik_v2-example-beta1@docker"],
"serverStatus": {
"http://10.0.1.10:80": "UP"
},
@@ -189,9 +195,7 @@
"provider": "docker",
"status": "enabled",
"type": "weighted",
- "usedBy": [
- "foo@docker"
- ],
+ "usedBy": ["foo@docker"],
"weighted": {
"sticky": {
"cookie": {
@@ -207,9 +211,7 @@
"provider": "docker",
"status": "enabled",
"type": "weighted",
- "usedBy": [
- "fii@docker"
- ],
+ "usedBy": ["fii@docker"],
"weighted": {
"sticky": {
"cookie": {}
@@ -238,8 +240,6 @@
"provider": "docker",
"status": "enabled",
"type": "mirroring",
- "usedBy": [
- "foo@docker"
- ]
+ "usedBy": ["foo@docker"]
}
]
diff --git a/webui/src/mocks/data/api-version.json b/webui/src/mocks/data/api-version.json
index d4577bdbf..4923a8856 100644
--- a/webui/src/mocks/data/api-version.json
+++ b/webui/src/mocks/data/api-version.json
@@ -1,6 +1,6 @@
{
- "Version": "3.4.0",
- "Codename": "montdor",
+ "Version": "3.6.0",
+ "Codename": "ramequin",
"disableDashboardAd": false,
"startDate": "2025-03-28T14:58:25.8937758+01:00"
}
\ No newline at end of file
diff --git a/webui/src/pages/http/HttpMiddleware.spec.tsx b/webui/src/pages/http/HttpMiddleware.spec.tsx
index 8bf003735..e70efbd62 100644
--- a/webui/src/pages/http/HttpMiddleware.spec.tsx
+++ b/webui/src/pages/http/HttpMiddleware.spec.tsx
@@ -1,12 +1,10 @@
-import { HttpMiddlewareRender } from './HttpMiddleware'
-
-import { ResourceDetailDataType } from 'hooks/use-resource-detail'
+import { MiddlewareDetail } from 'components/middlewares/MiddlewareDetail'
import { renderWithProviders } from 'utils/test'
describe('', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/http/middlewares/mock-middleware', withPage: true },
)
expect(getByTestId('error-text')).toBeInTheDocument()
@@ -14,7 +12,7 @@ describe('', () => {
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/http/middlewares/mock-middleware', withPage: true },
)
expect(getByTestId('skeleton')).toBeInTheDocument()
@@ -22,7 +20,7 @@ describe('', () => {
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/http/middlewares/mock-middleware', withPage: true },
)
expect(getByTestId('Not found page')).toBeInTheDocument()
@@ -55,7 +53,7 @@ describe('', () => {
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/http/middlewares/middleware-simple', withPage: true },
)
@@ -67,12 +65,11 @@ describe('', () => {
expect(middlewareCard.innerHTML).toContain('addprefix')
expect(middlewareCard.querySelector('svg[data-testid="docker"]')).toBeTruthy()
expect(middlewareCard.innerHTML).toContain('Success')
- expect(middlewareCard.innerHTML).toContain('/foo')
+ expect(container.innerHTML).toContain('/foo')
const routersTable = getByTestId('routers-table')
- const tableBody = routersTable.querySelectorAll('div[role="rowgroup"]')[1]
- expect(tableBody?.querySelectorAll('a[role="row"]')).toHaveLength(1)
- expect(tableBody?.innerHTML).toContain('router-test-simple@docker')
+ expect(routersTable.querySelectorAll('a[role="row"]')).toHaveLength(1)
+ expect(routersTable.innerHTML).toContain('router-test-simple@docker')
})
it('should render a plugin middleware', () => {
@@ -102,7 +99,7 @@ describe('', () => {
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/http/middlewares/middleware-plugin', withPage: true },
)
@@ -112,11 +109,11 @@ describe('', () => {
const middlewareCard = getByTestId('middleware-card')
expect(middlewareCard.innerHTML).toContain('jwtAuth')
+ expect(middlewareCard.innerHTML).toContain('Success')
const routersTable = getByTestId('routers-table')
- const tableBody = routersTable.querySelectorAll('div[role="rowgroup"]')[1]
- expect(tableBody?.querySelectorAll('a[role="row"]')).toHaveLength(1)
- expect(tableBody?.innerHTML).toContain('router-test-plugin@docker')
+ expect(routersTable.querySelectorAll('a[role="row"]')).toHaveLength(1)
+ expect(routersTable.innerHTML).toContain('router-test-plugin@docker')
})
it('should render a complex middleware', async () => {
@@ -342,7 +339,7 @@ describe('', () => {
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/http/middlewares/middleware-complex', withPage: true },
)
@@ -353,60 +350,50 @@ describe('', () => {
const middlewareCard = getByTestId('middleware-card')
expect(middlewareCard.innerHTML).toContain('Success')
expect(middlewareCard.innerHTML).toContain('the-provider')
- expect(middlewareCard.innerHTML).toContain('redirect-scheme')
- expect(middlewareCard.innerHTML).toContain('add-prefix-sample')
- expect(middlewareCard.innerHTML).toContain('buffer-retry-expression')
- expect(middlewareCard.innerHTML).toContain('circuit-breaker')
- expect(middlewareCard.innerHTML).toIncludeMultiple(['replace-path-regex', 'replace-path-replacement'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['/redirect-from-regex', '/redirect-to'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['127.0.0.1', '127.0.0.2', 'rate-limit-req-header'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['126.0.0.1', '126.0.0.2', 'inflight-req-header'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['125.0.0.1', '125.0.0.2', '125.0.0.3', '125.0.0.4'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['ssl.host', 'ssl-proxy-header-a', 'ssl-proxy-header-b'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['host-proxy-header-a', 'host-proxy-header-b'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['allowed-host-1', 'allowed-host-2'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['exposed-header-1', 'exposed-header-2'])
- expect(middlewareCard.innerHTML).toContain('allowed.origin')
- expect(middlewareCard.innerHTML).toContain('custom-frame-options')
- expect(middlewareCard.innerHTML).toContain('content-security-policy')
- expect(middlewareCard.innerHTML).toContain('public-key')
- expect(middlewareCard.innerHTML).toContain('referrer-policy')
- expect(middlewareCard.innerHTML).toContain('feature-policy')
- expect(middlewareCard.innerHTML).toIncludeMultiple(['GET', 'POST', 'PUT'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['allowed-header-1', 'allowed-header-2'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['custom-res-headers-a', 'custom-res-headers-b'])
- expect(middlewareCard.innerHTML).toIncludeMultiple(['custom-req-headers-a', 'custom-req-headers-b'])
- expect(middlewareCard.innerHTML).toIncludeMultiple([
+ expect(container.innerHTML).toContain('redirect-scheme')
+ expect(container.innerHTML).toContain('add-prefix-sample')
+ expect(container.innerHTML).toContain('buffer-retry-expression')
+ expect(container.innerHTML).toContain('circuit-breaker')
+ expect(container.innerHTML).toIncludeMultiple(['replace-path-regex', 'replace-path-replacement'])
+ expect(container.innerHTML).toIncludeMultiple(['/redirect-from-regex', '/redirect-to'])
+ expect(container.innerHTML).toIncludeMultiple(['127.0.0.1', '127.0.0.2', 'rate-limit-req-header'])
+ expect(container.innerHTML).toIncludeMultiple(['126.0.0.1', '126.0.0.2', 'inflight-req-header'])
+ expect(container.innerHTML).toIncludeMultiple(['125.0.0.1', '125.0.0.2', '125.0.0.3', '125.0.0.4'])
+ expect(container.innerHTML).toIncludeMultiple(['ssl.host', 'ssl-proxy-header-a', 'ssl-proxy-header-b'])
+ expect(container.innerHTML).toIncludeMultiple(['host-proxy-header-a', 'host-proxy-header-b'])
+ expect(container.innerHTML).toIncludeMultiple(['allowed-host-1', 'allowed-host-2'])
+ expect(container.innerHTML).toIncludeMultiple(['exposed-header-1', 'exposed-header-2'])
+ expect(container.innerHTML).toContain('allowed.origin')
+ expect(container.innerHTML).toContain('custom-frame-options')
+ expect(container.innerHTML).toContain('content-security-policy')
+ expect(container.innerHTML).toContain('public-key')
+ expect(container.innerHTML).toContain('referrer-policy')
+ expect(container.innerHTML).toContain('feature-policy')
+ expect(container.innerHTML).toIncludeMultiple(['GET', 'POST', 'PUT'])
+ expect(container.innerHTML).toIncludeMultiple(['allowed-header-1', 'allowed-header-2'])
+ expect(container.innerHTML).toIncludeMultiple(['custom-res-headers-a', 'custom-res-headers-b'])
+ expect(container.innerHTML).toIncludeMultiple(['custom-req-headers-a', 'custom-req-headers-b'])
+ expect(container.innerHTML).toIncludeMultiple([
'forward-auth-address',
'auth-response-header-1',
'auth-response-header-2',
])
- expect(middlewareCard.innerHTML).toIncludeMultiple([
+ expect(container.innerHTML).toIncludeMultiple([
'error-sample',
'status-1',
'status-2',
'errors-service',
'errors-query',
])
- expect(middlewareCard.innerHTML).toIncludeMultiple([
- 'chain-middleware-1',
- 'chain-middleware-2',
- 'chain-middleware-3',
- ])
- expect(middlewareCard.innerHTML).toIncludeMultiple([
- 'user1',
- 'user2',
- 'users/file',
- 'realm-sample',
- 'basic-auth-header',
- ])
- expect(middlewareCard.innerHTML).toIncludeMultiple([
+ expect(container.innerHTML).toIncludeMultiple(['chain-middleware-1', 'chain-middleware-2', 'chain-middleware-3'])
+ expect(container.innerHTML).toIncludeMultiple(['user1', 'user2', 'users/file', 'realm-sample', 'basic-auth-header'])
+ expect(container.innerHTML).toIncludeMultiple([
'strip-prefix1',
'strip-prefix2',
'strip-prefix-regex1',
'strip-prefix-regex2',
])
- expect(middlewareCard.innerHTML).toIncludeMultiple([
+ expect(container.innerHTML).toIncludeMultiple([
'10000',
'10001',
'10002',
@@ -421,7 +408,7 @@ describe('', () => {
'10011',
'10012',
])
- expect(middlewareCard.innerHTML).toIncludeMultiple([
+ expect(container.innerHTML).toIncludeMultiple([
'plugin-ldap-source',
'plugin-ldap-base-dn',
'plugin-ldap-attribute',
@@ -438,9 +425,8 @@ describe('', () => {
])
const routersTable = getByTestId('routers-table')
- const tableBody = routersTable.querySelectorAll('div[role="rowgroup"]')[1]
- expect(tableBody?.querySelectorAll('a[role="row"]')).toHaveLength(1)
- expect(tableBody?.innerHTML).toContain('router-test-complex@docker')
+ expect(routersTable.querySelectorAll('a[role="row"]')).toHaveLength(1)
+ expect(routersTable.innerHTML).toContain('router-test-complex@docker')
})
it('should render a plugin middleware with no type', async () => {
@@ -464,7 +450,7 @@ describe('', () => {
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/http/middlewares/middleware-plugin-no-type', withPage: true },
)
@@ -474,15 +460,15 @@ describe('', () => {
const middlewareCard = getByTestId('middleware-card')
expect(middlewareCard.innerHTML).toContain('Success')
- expect(middlewareCard.innerHTML).toContain('jwtAuth > child')
- expect(middlewareCard.innerHTML).toContain('jwtAuth > sibling > negative Grand Child')
- expect(middlewareCard.innerHTML).toContain('jwtAuth > sibling > positive Grand Child')
- expect(middlewareCard.innerHTML).toContain('jwtAuth > string Child')
- expect(middlewareCard.innerHTML).toContain('jwtAuth > array Child')
+ expect(container.innerHTML).toContain('jwtAuth > child')
+ expect(container.innerHTML).toContain('jwtAuth > sibling > negative Grand Child')
+ expect(container.innerHTML).toContain('jwtAuth > sibling > positive Grand Child')
+ expect(container.innerHTML).toContain('jwtAuth > string Child')
+ expect(container.innerHTML).toContain('jwtAuth > array Child')
- const childSpans = Array.from(middlewareCard.querySelectorAll('span')).filter((span) =>
+ const childSpans = Array.from(container.querySelectorAll('span')).filter((span) =>
['0', '1', '2', '3', '123'].includes(span.innerHTML),
)
- expect(childSpans.length).toBe(7)
+ expect(childSpans.length).toBe(6)
})
})
diff --git a/webui/src/pages/http/HttpMiddleware.tsx b/webui/src/pages/http/HttpMiddleware.tsx
index d762e6975..511ac190d 100644
--- a/webui/src/pages/http/HttpMiddleware.tsx
+++ b/webui/src/pages/http/HttpMiddleware.tsx
@@ -1,82 +1,12 @@
-import { Box, Card, H1, Skeleton, styled, Text } from '@traefiklabs/faency'
-import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
-import { DetailSectionSkeleton } from 'components/resources/DetailSections'
-import { RenderMiddleware } from 'components/resources/MiddlewarePanel'
-import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection'
-import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail'
-import { NotFound } from 'pages/NotFound'
-import breakpoints from 'utils/breakpoints'
-
-const MiddlewareGrid = styled(Box, {
- display: 'grid',
- gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))',
-
- [`@media (max-width: ${breakpoints.tablet})`]: {
- gridTemplateColumns: '1fr',
- },
-})
-
-type HttpMiddlewareRenderProps = {
- data?: ResourceDetailDataType
- error?: Error | null
- name: string
-}
-
-export const HttpMiddlewareRender = ({ data, error, name }: HttpMiddlewareRenderProps) => {
- if (error) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
- Sorry, we could not fetch detail information for this Middleware right now. Please, try again later.
-
- >
- )
- }
-
- if (!data) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
-
-
-
-
- >
- )
- }
-
- if (!data.name) {
- return
- }
-
- return (
- <>
-
- {data.name} - Traefik Proxy
-
- {data.name}
-
-
-
-
-
-
- >
- )
-}
+import { MiddlewareDetail } from 'components/middlewares/MiddlewareDetail'
+import { useResourceDetail } from 'hooks/use-resource-detail'
export const HttpMiddleware = () => {
const { name } = useParams<{ name: string }>()
const { data, error } = useResourceDetail(name!, 'middlewares')
- return
+ return
}
export default HttpMiddleware
diff --git a/webui/src/pages/http/HttpMiddlewares.tsx b/webui/src/pages/http/HttpMiddlewares.tsx
index 4ec86d841..5df1633ee 100644
--- a/webui/src/pages/http/HttpMiddlewares.tsx
+++ b/webui/src/pages/http/HttpMiddlewares.tsx
@@ -1,17 +1,16 @@
-import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency'
+import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Flex } from '@traefiklabs/faency'
import { useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
import useInfiniteScroll from 'react-infinite-scroll-hook'
import { useSearchParams } from 'react-router-dom'
-import ClickableRow from 'components/ClickableRow'
-import ProviderIcon from 'components/icons/providers'
+import { ScrollTopButton } from 'components/buttons/ScrollTopButton'
+import { ProviderIconWithTooltip } from 'components/icons/providers'
import { ResourceStatus } from 'components/resources/ResourceStatus'
-import { ScrollTopButton } from 'components/ScrollTopButton'
import { SpinnerLoader } from 'components/SpinnerLoader'
-import { searchParamsToState, TableFilter } from 'components/TableFilter'
+import ClickableRow from 'components/tables/ClickableRow'
import SortableTh from 'components/tables/SortableTh'
-import Tooltip from 'components/Tooltip'
+import { searchParamsToState, TableFilter } from 'components/tables/TableFilter'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
@@ -24,11 +23,7 @@ export const makeRowRender = (): RenderRowType => {
return (
-
-
-
-
-
+
@@ -37,11 +32,7 @@ export const makeRowRender = (): RenderRowType => {
-
-
-
-
-
+
)
@@ -69,7 +60,7 @@ export const HttpMiddlewaresRender = ({
-
+
diff --git a/webui/src/pages/http/HttpRouter.spec.tsx b/webui/src/pages/http/HttpRouter.spec.tsx
index ca90455d9..3a0690232 100644
--- a/webui/src/pages/http/HttpRouter.spec.tsx
+++ b/webui/src/pages/http/HttpRouter.spec.tsx
@@ -1,6 +1,4 @@
-import { HttpRouterRender } from './HttpRouter'
-
-import { ResourceDetailDataType } from 'hooks/use-resource-detail'
+import { RouterDetail } from 'components/routers/RouterDetail'
import apiEntrypoints from 'mocks/data/api-entrypoints.json'
import apiHttpMiddlewares from 'mocks/data/api-http_middlewares.json'
import apiHttpRouters from 'mocks/data/api-http_routers.json'
@@ -9,7 +7,7 @@ import { renderWithProviders } from 'utils/test'
describe('', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/http/routers/mock-router', withPage: true },
)
expect(getByTestId('error-text')).toBeInTheDocument()
@@ -17,7 +15,7 @@ describe('', () => {
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/http/routers/mock-router', withPage: true },
)
expect(getByTestId('skeleton')).toBeInTheDocument()
@@ -25,7 +23,7 @@ describe('', () => {
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/http/routers/mock-router', withPage: true },
)
expect(getByTestId('Not found page')).toBeInTheDocument()
@@ -42,7 +40,7 @@ describe('', () => {
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/http/routers/orphan-router@file', withPage: true },
)
@@ -52,7 +50,6 @@ describe('', () => {
expect(routerStructure.innerHTML).toContain(':8080')
expect(routerStructure.innerHTML).toContain(':8002')
expect(routerStructure.innerHTML).toContain(':8003')
- expect(routerStructure.innerHTML).toContain('orphan-router@file')
expect(routerStructure.innerHTML).toContain('middleware00')
expect(routerStructure.innerHTML).toContain('middleware01')
expect(routerStructure.innerHTML).toContain('middleware02')
@@ -78,43 +75,35 @@ describe('', () => {
expect(routerStructure.innerHTML).toContain('HTTP Router')
expect(routerStructure.innerHTML).not.toContain('TCP Router')
- const routerDetailsSection = getByTestId('router-detail')
+ const routerDetailsSection = getByTestId('router-details')
- const routerDetailsPanel = routerDetailsSection.querySelector(':scope > div:nth-child(1)')
- expect(routerDetailsPanel?.innerHTML).toContain('orphan-router@file')
- expect(routerDetailsPanel?.innerHTML).toContain('Error')
- expect(routerDetailsPanel?.querySelector('svg[data-testid="file"]')).toBeTruthy()
- expect(routerDetailsPanel?.innerHTML).toContain(
+ expect(routerDetailsSection?.innerHTML).toContain('Error')
+ expect(routerDetailsSection?.querySelector('svg[data-testid="file"]')).toBeTruthy()
+ expect(routerDetailsSection?.innerHTML).toContain(
'Path(`somethingreallyunexpectedbutalsoverylongitgetsoutofthecontainermaybe`)',
)
- expect(routerDetailsPanel?.innerHTML).toContain('unexistingservice')
- expect(routerDetailsPanel?.innerHTML).toContain('the service "unexistingservice@file" does not exist')
- const middlewaresPanel = routerDetailsSection.querySelector(':scope > div:nth-child(3)')
- const providers = Array.from(middlewaresPanel?.querySelectorAll('svg[data-testid="docker"]') || [])
- expect(middlewaresPanel?.innerHTML).toContain('middleware00')
- expect(middlewaresPanel?.innerHTML).toContain('middleware01')
- expect(middlewaresPanel?.innerHTML).toContain('middleware02')
- expect(middlewaresPanel?.innerHTML).toContain('middleware03')
- expect(middlewaresPanel?.innerHTML).toContain('middleware04')
- expect(middlewaresPanel?.innerHTML).toContain('middleware05')
- expect(middlewaresPanel?.innerHTML).toContain('middleware06')
- expect(middlewaresPanel?.innerHTML).toContain('middleware07')
- expect(middlewaresPanel?.innerHTML).toContain('middleware08')
- expect(middlewaresPanel?.innerHTML).toContain('middleware09')
- expect(middlewaresPanel?.innerHTML).toContain('middleware10')
- expect(middlewaresPanel?.innerHTML).toContain('middleware11')
- expect(middlewaresPanel?.innerHTML).toContain('middleware12')
- expect(middlewaresPanel?.innerHTML).toContain('middleware13')
- expect(middlewaresPanel?.innerHTML).toContain('middleware14')
- expect(middlewaresPanel?.innerHTML).toContain('middleware15')
- expect(middlewaresPanel?.innerHTML).toContain('middleware16')
- expect(middlewaresPanel?.innerHTML).toContain('middleware17')
- expect(middlewaresPanel?.innerHTML).toContain('middleware18')
- expect(middlewaresPanel?.innerHTML).toContain('middleware19')
- expect(middlewaresPanel?.innerHTML).toContain('middleware20')
- expect(middlewaresPanel?.innerHTML).toContain('Success')
- expect(providers.length).toBe(21)
+ expect(routerStructure.innerHTML).toContain('middleware00')
+ expect(routerStructure.innerHTML).toContain('middleware01')
+ expect(routerStructure.innerHTML).toContain('middleware02')
+ expect(routerStructure.innerHTML).toContain('middleware03')
+ expect(routerStructure.innerHTML).toContain('middleware04')
+ expect(routerStructure.innerHTML).toContain('middleware05')
+ expect(routerStructure.innerHTML).toContain('middleware06')
+ expect(routerStructure.innerHTML).toContain('middleware07')
+ expect(routerStructure.innerHTML).toContain('middleware08')
+ expect(routerStructure.innerHTML).toContain('middleware09')
+ expect(routerStructure.innerHTML).toContain('middleware10')
+ expect(routerStructure.innerHTML).toContain('middleware11')
+ expect(routerStructure.innerHTML).toContain('middleware12')
+ expect(routerStructure.innerHTML).toContain('middleware13')
+ expect(routerStructure.innerHTML).toContain('middleware14')
+ expect(routerStructure.innerHTML).toContain('middleware15')
+ expect(routerStructure.innerHTML).toContain('middleware16')
+ expect(routerStructure.innerHTML).toContain('middleware17')
+ expect(routerStructure.innerHTML).toContain('middleware18')
+ expect(routerStructure.innerHTML).toContain('middleware19')
+ expect(routerStructure.innerHTML).toContain('middleware20')
expect(getByTestId('/http/middlewares/middleware00@docker')).toBeInTheDocument()
diff --git a/webui/src/pages/http/HttpRouter.tsx b/webui/src/pages/http/HttpRouter.tsx
index 62801083a..dc4b8de63 100644
--- a/webui/src/pages/http/HttpRouter.tsx
+++ b/webui/src/pages/http/HttpRouter.tsx
@@ -1,161 +1,13 @@
-import { Flex, styled, Text } from '@traefiklabs/faency'
-import { useContext, useEffect, useMemo } from 'react'
-import { Helmet } from 'react-helmet-async'
-import { FiGlobe, FiLayers, FiLogIn, FiZap } from 'react-icons/fi'
import { useParams } from 'react-router-dom'
-import { CardListSection, DetailSectionSkeleton } from 'components/resources/DetailSections'
-import MiddlewarePanel from 'components/resources/MiddlewarePanel'
-import RouterPanel from 'components/resources/RouterPanel'
-import TlsPanel from 'components/resources/TlsPanel'
-import { ToastContext } from 'contexts/toasts'
-import { EntryPoint, ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail'
-import { getErrorData, getValidData } from 'libs/objectHandlers'
-import { parseMiddlewareType } from 'libs/parsers'
-import { NotFound } from 'pages/NotFound'
-
-const CardListColumns = styled(Flex, {
- display: 'grid',
- gridTemplateColumns: 'repeat(4, 1fr)',
- marginBottom: '48px',
-})
-
-type DetailProps = {
- data: ResourceDetailDataType
- protocol?: string
-}
-
-export const RouterStructure = ({ data, protocol = 'http' }: DetailProps) => {
- const { addToast } = useContext(ToastContext)
- const entrypoints = useMemo(() => getValidData(data.entryPointsData), [data?.entryPointsData])
- const entrypointsError = useMemo(() => getErrorData(data.entryPointsData), [data?.entryPointsData])
-
- const serviceSlug = data.service?.includes('@')
- ? data.service
- : `${data.service ?? 'unknown'}@${data.provider ?? 'unknown'}`
-
- useEffect(() => {
- entrypointsError?.map((error) =>
- addToast({
- message: error.message,
- severity: 'error',
- }),
- )
- }, [addToast, entrypointsError])
-
- return (
-
- {entrypoints.length > 0 && (
- }
- title="Entrypoints"
- cards={data.entryPointsData?.map((ep: EntryPoint) => ({
- title: ep.name,
- description: ep.address,
- }))}
- />
- )}
- }
- title={`${protocol.toUpperCase()} Router`}
- cards={[{ title: 'router', description: data.name, focus: true }]}
- />
- {data.hasValidMiddlewares && (
- }
- title={`${protocol.toUpperCase()} Middlewares`}
- cards={data.middlewares?.map((mw) => ({
- title: parseMiddlewareType(mw) ?? 'middleware',
- description: mw.name,
- link: `/${protocol}/middlewares/${mw.name}`,
- }))}
- />
- )}
- }
- title="Service"
- cards={[{ title: 'service', description: data.service, link: `/${protocol}/services/${serviceSlug}` }]}
- />
-
- )
-}
-
-const SpacedColumns = styled(Flex, {
- display: 'grid',
- gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
- gridGap: '16px',
-})
-
-const RouterDetail = ({ data }: DetailProps) => (
-
-
-
-
-
-)
-
-type HttpRouterRenderProps = {
- data?: ResourceDetailDataType
- error?: Error | null
- name: string
-}
-
-export const HttpRouterRender = ({ data, error, name }: HttpRouterRenderProps) => {
- if (error) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
- Sorry, we could not fetch detail information for this Router right now. Please, try again later.
-
- >
- )
- }
-
- if (!data) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )
- }
-
- if (!data.name) {
- return
- }
-
- return (
- <>
-
- {data.name} - Traefik Proxy
-
-
-
- >
- )
-}
+import { RouterDetail } from 'components/routers/RouterDetail'
+import { useResourceDetail } from 'hooks/use-resource-detail'
export const HttpRouter = () => {
const { name } = useParams<{ name: string }>()
const { data, error } = useResourceDetail(name!, 'routers')
- return
+
+ return
}
export default HttpRouter
diff --git a/webui/src/pages/http/HttpRouters.tsx b/webui/src/pages/http/HttpRouters.tsx
index 7896ca38a..ed84df5a3 100644
--- a/webui/src/pages/http/HttpRouters.tsx
+++ b/webui/src/pages/http/HttpRouters.tsx
@@ -1,18 +1,18 @@
import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency'
import { useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
-import { FiShield } from 'react-icons/fi'
import useInfiniteScroll from 'react-infinite-scroll-hook'
import { useSearchParams } from 'react-router-dom'
-import ClickableRow from 'components/ClickableRow'
-import ProviderIcon from 'components/icons/providers'
-import { Chips } from 'components/resources/DetailSections'
+import { ScrollTopButton } from 'components/buttons/ScrollTopButton'
+import { ProviderIconWithTooltip } from 'components/icons/providers'
+import { Chips } from 'components/resources/DetailItemComponents'
import { ResourceStatus } from 'components/resources/ResourceStatus'
-import { ScrollTopButton } from 'components/ScrollTopButton'
+import TlsIcon from 'components/routers/TlsIcon'
import { SpinnerLoader } from 'components/SpinnerLoader'
-import { searchParamsToState, TableFilter } from 'components/TableFilter'
+import ClickableRow from 'components/tables/ClickableRow'
import SortableTh from 'components/tables/SortableTh'
+import { searchParamsToState, TableFilter } from 'components/tables/TableFilter'
import Tooltip from 'components/Tooltip'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
@@ -22,25 +22,32 @@ export const makeRowRender = (protocol = 'http'): RenderRowType => {
const HttpRoutersRenderRow = (row) => (
-
-
-
-
-
+
{protocol !== 'udp' && (
<>
{row.tls && (
-
-
+
+
)}
-
+
>
)}
@@ -52,11 +59,7 @@ export const makeRowRender = (protocol = 'http'): RenderRowType => {
-
-
-
-
-
+
@@ -86,8 +89,8 @@ export const HttpRoutersRender = ({
-
-
+
+
diff --git a/webui/src/pages/http/HttpService.spec.tsx b/webui/src/pages/http/HttpService.spec.tsx
index 76059e21d..d4b98500a 100644
--- a/webui/src/pages/http/HttpService.spec.tsx
+++ b/webui/src/pages/http/HttpService.spec.tsx
@@ -1,12 +1,10 @@
-import { HttpServiceRender } from './HttpService'
-
-import { ResourceDetailDataType } from 'hooks/use-resource-detail'
+import { ServiceDetail } from 'components/services/ServiceDetail'
import { renderWithProviders } from 'utils/test'
describe('', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/http/services/mock-service', withPage: true },
)
expect(getByTestId('error-text')).toBeInTheDocument()
@@ -14,7 +12,7 @@ describe('', () => {
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/http/services/mock-service', withPage: true },
)
expect(getByTestId('skeleton')).toBeInTheDocument()
@@ -22,7 +20,7 @@ describe('', () => {
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/http/services/mock-service', withPage: true },
)
expect(getByTestId('Not found page')).toBeInTheDocument()
@@ -73,7 +71,7 @@ describe('', () => {
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/http/services/mock-service', withPage: true },
)
@@ -88,7 +86,7 @@ describe('', () => {
expect(serviceDetails.innerHTML).toContain('docker')
expect(serviceDetails.innerHTML).toContain('Status')
expect(serviceDetails.innerHTML).toContain('Success')
- expect(serviceDetails.innerHTML).toContain('Pass Host Header')
+ expect(serviceDetails.innerHTML).toContain('Pass host header')
expect(serviceDetails.innerHTML).toContain('True')
const serversList = getByTestId('servers-list')
@@ -96,10 +94,9 @@ describe('', () => {
expect(serversList.innerHTML).toContain('http://10.0.1.12:80')
const routersTable = getByTestId('routers-table')
- const tableBody = routersTable.querySelectorAll('div[role="rowgroup"]')[1]
- expect(tableBody?.querySelectorAll('a[role="row"]')).toHaveLength(2)
- expect(tableBody?.innerHTML).toContain('router-test1@docker')
- expect(tableBody?.innerHTML).toContain('router-test2@docker')
+ expect(routersTable.querySelectorAll('a[role="row"]')).toHaveLength(2)
+ expect(routersTable.innerHTML).toContain('router-test1@docker')
+ expect(routersTable.innerHTML).toContain('router-test2@docker')
expect(() => {
getByTestId('health-check')
@@ -145,7 +142,7 @@ describe('', () => {
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/http/services/mock-service', withPage: true },
)
@@ -200,7 +197,7 @@ describe('', () => {
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/http/services/mock-service', withPage: true },
)
diff --git a/webui/src/pages/http/HttpService.tsx b/webui/src/pages/http/HttpService.tsx
index 1761cbf05..aac9cb13d 100644
--- a/webui/src/pages/http/HttpService.tsx
+++ b/webui/src/pages/http/HttpService.tsx
@@ -1,323 +1,13 @@
-import { Badge, Box, Flex, H1, Skeleton, styled, Text } from '@traefiklabs/faency'
-import { useMemo } from 'react'
-import { Helmet } from 'react-helmet-async'
-import { FiGlobe, FiInfo, FiShield } from 'react-icons/fi'
import { useParams } from 'react-router-dom'
-import ProviderIcon from 'components/icons/providers'
-import {
- BooleanState,
- Chips,
- DetailSection,
- DetailSectionSkeleton,
- ItemBlock,
- ItemTitle,
- LayoutTwoCols,
- ProviderName,
-} from 'components/resources/DetailSections'
-import { ResourceStatus } from 'components/resources/ResourceStatus'
-import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection'
-import Tooltip from 'components/Tooltip'
-import { ResourceDetailDataType, ServiceDetailType, useResourceDetail } from 'hooks/use-resource-detail'
-import { NotFound } from 'pages/NotFound'
-
-type DetailProps = {
- data: ServiceDetailType
- protocol?: string
-}
-
-const SpacedColumns = styled(Flex, {
- display: 'grid',
- gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
- gridGap: '16px',
-})
-
-const ServicesGrid = styled(Box, {
- display: 'grid',
- gridTemplateColumns: '2fr 1fr 1fr',
- alignItems: 'center',
- padding: '$3 $5',
- borderBottom: '1px solid $tableRowBorder',
-})
-
-const ServersGrid = styled(Box, {
- display: 'grid',
- alignItems: 'center',
- padding: '$3 $5',
- borderBottom: '1px solid $tableRowBorder',
-})
-
-const MirrorsGrid = styled(Box, {
- display: 'grid',
- gridTemplateColumns: '2fr 1fr 1fr',
- alignItems: 'center',
- padding: '$3 $5',
- borderBottom: '1px solid $tableRowBorder',
-
- '> *:not(:first-child)': {
- justifySelf: 'flex-end',
- },
-})
-
-const GridTitle = styled(Text, {
- fontSize: '14px',
- fontWeight: 700,
- color: 'hsl(0, 0%, 56%)',
-})
-
-type Server = {
- url: string
- address?: string
-}
-
-type ServerStatus = {
- [server: string]: string
-}
-
-function getServerStatusList(data: ServiceDetailType): ServerStatus {
- const serversList: ServerStatus = {}
-
- data.loadBalancer?.servers?.forEach((server: Server) => {
- serversList[server.address || server.url] = 'DOWN'
- })
-
- if (data.serverStatus) {
- Object.entries(data.serverStatus).forEach(([server, status]) => {
- serversList[server] = status
- })
- }
-
- return serversList
-}
-
-export const ServicePanels = ({ data, protocol = '' }: DetailProps) => {
- const serversList = getServerStatusList(data)
- const getProviderFromName = (serviceName: string): string => {
- const [, provider] = serviceName.split('@')
- return provider || data.provider
- }
- const providerName = useMemo(() => {
- return data.provider
- }, [data.provider])
-
- return (
-
- } title="Service Details">
-
- {data.type && (
-
- {data.type}
-
- )}
- {data.provider && (
-
-
- {providerName}
-
- )}
-
- {data.status && (
-
-
-
- )}
- {data.mirroring && data.mirroring.service && (
-
- {data.mirroring.service}
-
- )}
- {data.loadBalancer && (
- <>
- {data.loadBalancer.passHostHeader && (
-
-
-
- )}
- {data.loadBalancer.terminationDelay && (
-
- {`${data.loadBalancer.terminationDelay} ms`}
-
- )}
- >
- )}
-
- {data.loadBalancer?.healthCheck && (
- } title="Health Check">
-
-
- {data.loadBalancer.healthCheck.scheme && (
-
- {data.loadBalancer.healthCheck.scheme}
-
- )}
- {data.loadBalancer.healthCheck.interval && (
-
- {data.loadBalancer.healthCheck.interval}
-
- )}
-
-
- {data.loadBalancer.healthCheck.path && (
-
-
- {data.loadBalancer.healthCheck.path}
-
-
- )}
- {data.loadBalancer.healthCheck.timeout && (
-
- {data.loadBalancer.healthCheck.timeout}
-
- )}
-
-
- {data.loadBalancer.healthCheck.port && (
-
- {data.loadBalancer.healthCheck.port}
-
- )}
- {data.loadBalancer.healthCheck.hostname && (
-
-
- {data.loadBalancer.healthCheck.hostname}
-
-
- )}
-
- {data.loadBalancer.healthCheck.headers && (
-
- entry.join(': '))}
- />
-
- )}
-
-
- )}
- {!!data?.weighted?.services?.length && (
- } title="Services" noPadding>
- <>
-
- Name
- Weight
- Provider
-
-
- {data.weighted.services.map((service) => (
-
- {service.name}
- {service.weight}
-
-
-
-
- ))}
-
- >
-
- )}
- {Object.keys(serversList).length > 0 && (
- } title="Servers" noPadding>
- <>
-
- {protocol === 'http' && Status}
- URL
-
-
- {Object.entries(serversList).map(([server, status]) => (
-
- {protocol === 'http' && }
-
-
- {server}
-
-
-
- ))}
-
- >
-
- )}
- {data.mirroring?.mirrors && data.mirroring.mirrors.length > 0 && (
- } title="Mirror Services" noPadding>
-
- Name
- Percent
- Provider
-
-
- {data.mirroring.mirrors.map((mirror) => (
-
- {mirror.name}
- {mirror.percent}
-
-
- ))}
-
-
- )}
-
- )
-}
-
-type HttpServiceRenderProps = {
- data?: ResourceDetailDataType
- error?: Error
- name: string
-}
-
-export const HttpServiceRender = ({ data, error, name }: HttpServiceRenderProps) => {
- if (error) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
- Sorry, we could not fetch detail information for this Service right now. Please, try again later.
-
- >
- )
- }
-
- if (!data) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
-
-
-
-
-
-
- >
- )
- }
-
- if (!data.name) {
- return
- }
-
- return (
- <>
-
- {data.name} - Traefik Proxy
-
- {data.name}
-
-
- >
- )
-}
+import { ServiceDetail } from 'components/services/ServiceDetail'
+import { useResourceDetail } from 'hooks/use-resource-detail'
export const HttpService = () => {
const { name } = useParams<{ name: string }>()
- const { data, error } = useResourceDetail(name!, 'services')
- return
+ const { data, error } = useResourceDetail(name ?? '', 'services')
+
+ return
}
export default HttpService
diff --git a/webui/src/pages/http/HttpServices.tsx b/webui/src/pages/http/HttpServices.tsx
index e49bc82b6..9864b69a1 100644
--- a/webui/src/pages/http/HttpServices.tsx
+++ b/webui/src/pages/http/HttpServices.tsx
@@ -1,17 +1,16 @@
-import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex, Text } from '@traefiklabs/faency'
+import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Flex, Text } from '@traefiklabs/faency'
import { useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
import useInfiniteScroll from 'react-infinite-scroll-hook'
import { useSearchParams } from 'react-router-dom'
-import ClickableRow from 'components/ClickableRow'
-import ProviderIcon from 'components/icons/providers'
+import { ScrollTopButton } from 'components/buttons/ScrollTopButton'
+import { ProviderIconWithTooltip } from 'components/icons/providers'
import { ResourceStatus } from 'components/resources/ResourceStatus'
-import { ScrollTopButton } from 'components/ScrollTopButton'
import { SpinnerLoader } from 'components/SpinnerLoader'
-import { searchParamsToState, TableFilter } from 'components/TableFilter'
+import ClickableRow from 'components/tables/ClickableRow'
import SortableTh from 'components/tables/SortableTh'
-import Tooltip from 'components/Tooltip'
+import { searchParamsToState, TableFilter } from 'components/tables/TableFilter'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
@@ -20,11 +19,7 @@ export const makeRowRender = (): RenderRowType => {
const HttpServicesRenderRow = (row) => (
-
-
-
-
-
+
@@ -36,11 +31,7 @@ export const makeRowRender = (): RenderRowType => {
{row.loadBalancer?.servers?.length || 0}
-
-
-
-
-
+
)
@@ -67,7 +58,7 @@ export const HttpServicesRender = ({
-
+
diff --git a/webui/src/pages/tcp/TcpMiddleware.spec.tsx b/webui/src/pages/tcp/TcpMiddleware.spec.tsx
index 73e69e01c..3e92ec293 100644
--- a/webui/src/pages/tcp/TcpMiddleware.spec.tsx
+++ b/webui/src/pages/tcp/TcpMiddleware.spec.tsx
@@ -1,12 +1,10 @@
-import { TcpMiddlewareRender } from './TcpMiddleware'
-
-import { ResourceDetailDataType } from 'hooks/use-resource-detail'
+import { MiddlewareDetail } from 'components/middlewares/MiddlewareDetail'
import { renderWithProviders } from 'utils/test'
describe('', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/tcp/middlewares/mock-middleware', withPage: true },
)
expect(getByTestId('error-text')).toBeInTheDocument()
@@ -14,7 +12,7 @@ describe('', () => {
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/tcp/middlewares/mock-middleware', withPage: true },
)
expect(getByTestId('skeleton')).toBeInTheDocument()
@@ -22,7 +20,7 @@ describe('', () => {
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/tcp/middlewares/mock-middleware', withPage: true },
)
expect(getByTestId('Not found page')).toBeInTheDocument()
@@ -55,7 +53,7 @@ describe('', () => {
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/tcp/middlewares/middleware-simple', withPage: true },
)
@@ -66,14 +64,13 @@ describe('', () => {
const middlewareCard = getByTestId('middleware-card')
expect(middlewareCard.querySelector('svg[data-testid="docker"]')).toBeTruthy()
expect(middlewareCard.innerHTML).toContain('Success')
- expect(middlewareCard.innerHTML).toContain('inFlightConn')
- expect(middlewareCard.innerHTML).toContain('amount')
- expect(middlewareCard.innerHTML).toContain('10')
+ expect(container.innerHTML).toContain('inFlightConn')
+ expect(container.innerHTML).toContain('amount')
+ expect(container.innerHTML).toContain('10')
const routersTable = getByTestId('routers-table')
- const tableBody = routersTable.querySelectorAll('div[role="rowgroup"]')[1]
- expect(tableBody?.querySelectorAll('a[role="row"]')).toHaveLength(1)
- expect(tableBody?.innerHTML).toContain('router-test-simple@docker')
+ expect(routersTable.querySelectorAll('a[role="row"]')).toHaveLength(1)
+ expect(routersTable.innerHTML).toContain('router-test-simple@docker')
})
it('should render a complex middleware', async () => {
@@ -106,7 +103,7 @@ describe('', () => {
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/tcp/middlewares/middleware-complex', withPage: true },
)
@@ -117,17 +114,16 @@ describe('', () => {
const middlewareCard = getByTestId('middleware-card')
expect(middlewareCard.innerHTML).toContain('Success')
expect(middlewareCard.innerHTML).toContain('the-provider')
- expect(middlewareCard.innerHTML).toContain('inFlightConn')
- expect(middlewareCard.innerHTML).toContain('amount')
- expect(middlewareCard.innerHTML).toContain('10')
- expect(middlewareCard.innerHTML).toContain('ipWhiteList')
- expect(middlewareCard.innerHTML).toContain('source Range')
- expect(middlewareCard.innerHTML).toContain('125.0.0.1')
- expect(middlewareCard.innerHTML).toContain('125.0.0.4')
+ expect(container.innerHTML).toContain('inFlightConn')
+ expect(container.innerHTML).toContain('amount')
+ expect(container.innerHTML).toContain('10')
+ expect(container.innerHTML).toContain('ipWhiteList')
+ expect(container.innerHTML).toContain('source Range')
+ expect(container.innerHTML).toContain('125.0.0.1')
+ expect(container.innerHTML).toContain('125.0.0.4')
const routersTable = getByTestId('routers-table')
- const tableBody = routersTable.querySelectorAll('div[role="rowgroup"]')[1]
- expect(tableBody?.querySelectorAll('a[role="row"]')).toHaveLength(1)
- expect(tableBody?.innerHTML).toContain('router-test-complex@docker')
+ expect(routersTable.querySelectorAll('a[role="row"]')).toHaveLength(1)
+ expect(routersTable.innerHTML).toContain('router-test-complex@docker')
})
})
diff --git a/webui/src/pages/tcp/TcpMiddleware.tsx b/webui/src/pages/tcp/TcpMiddleware.tsx
index d85cf22ec..24a41f729 100644
--- a/webui/src/pages/tcp/TcpMiddleware.tsx
+++ b/webui/src/pages/tcp/TcpMiddleware.tsx
@@ -1,82 +1,12 @@
-import { Card, Box, H1, Skeleton, styled, Text } from '@traefiklabs/faency'
-import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
-import { DetailSectionSkeleton } from 'components/resources/DetailSections'
-import { RenderMiddleware } from 'components/resources/MiddlewarePanel'
-import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection'
-import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail'
-import { NotFound } from 'pages/NotFound'
-import breakpoints from 'utils/breakpoints'
-
-const MiddlewareGrid = styled(Box, {
- display: 'grid',
- gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))',
-
- [`@media (max-width: ${breakpoints.tablet})`]: {
- gridTemplateColumns: '1fr',
- },
-})
-
-type TcpMiddlewareRenderProps = {
- data?: ResourceDetailDataType
- error?: Error
- name: string
-}
-
-export const TcpMiddlewareRender = ({ data, error, name }: TcpMiddlewareRenderProps) => {
- if (error) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
- Sorry, we could not fetch detail information for this Middleware right now. Please, try again later.
-
- >
- )
- }
-
- if (!data) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
-
-
-
-
- >
- )
- }
-
- if (!data.name) {
- return
- }
-
- return (
- <>
-
- {data.name} - Traefik Proxy
-
- {data.name}
-
-
-
-
-
-
- >
- )
-}
+import { MiddlewareDetail } from 'components/middlewares/MiddlewareDetail'
+import { useResourceDetail } from 'hooks/use-resource-detail'
export const TcpMiddleware = () => {
const { name } = useParams<{ name: string }>()
const { data, error } = useResourceDetail(name!, 'middlewares', 'tcp')
- return
+ return
}
export default TcpMiddleware
diff --git a/webui/src/pages/tcp/TcpMiddlewares.tsx b/webui/src/pages/tcp/TcpMiddlewares.tsx
index 25bca597b..bb2880fa2 100644
--- a/webui/src/pages/tcp/TcpMiddlewares.tsx
+++ b/webui/src/pages/tcp/TcpMiddlewares.tsx
@@ -1,17 +1,16 @@
-import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency'
+import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Flex } from '@traefiklabs/faency'
import { useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
import useInfiniteScroll from 'react-infinite-scroll-hook'
import { useSearchParams } from 'react-router-dom'
-import ClickableRow from 'components/ClickableRow'
-import ProviderIcon from 'components/icons/providers'
+import { ScrollTopButton } from 'components/buttons/ScrollTopButton'
+import { ProviderIconWithTooltip } from 'components/icons/providers'
import { ResourceStatus } from 'components/resources/ResourceStatus'
-import { ScrollTopButton } from 'components/ScrollTopButton'
import { SpinnerLoader } from 'components/SpinnerLoader'
-import { searchParamsToState, TableFilter } from 'components/TableFilter'
+import ClickableRow from 'components/tables/ClickableRow'
import SortableTh from 'components/tables/SortableTh'
-import Tooltip from 'components/Tooltip'
+import { searchParamsToState, TableFilter } from 'components/tables/TableFilter'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
@@ -24,11 +23,7 @@ export const makeRowRender = (): RenderRowType => {
return (
-
-
-
-
-
+
@@ -37,11 +32,7 @@ export const makeRowRender = (): RenderRowType => {
-
-
-
-
-
+
)
@@ -69,7 +60,7 @@ export const TcpMiddlewaresRender = ({
-
+
diff --git a/webui/src/pages/tcp/TcpRouter.spec.tsx b/webui/src/pages/tcp/TcpRouter.spec.tsx
index c1f5bb6a4..3423b6626 100644
--- a/webui/src/pages/tcp/TcpRouter.spec.tsx
+++ b/webui/src/pages/tcp/TcpRouter.spec.tsx
@@ -1,12 +1,10 @@
-import { TcpRouterRender } from './TcpRouter'
-
-import { ResourceDetailDataType } from 'hooks/use-resource-detail'
+import { RouterDetail } from 'components/routers/RouterDetail'
import { renderWithProviders } from 'utils/test'
describe('', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/tcp/routers/mock-router', withPage: true },
)
expect(getByTestId('error-text')).toBeInTheDocument()
@@ -14,7 +12,7 @@ describe('', () => {
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/tcp/routers/mock-router', withPage: true },
)
expect(getByTestId('skeleton')).toBeInTheDocument()
@@ -22,7 +20,7 @@ describe('', () => {
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/tcp/routers/mock-router', withPage: true },
)
expect(getByTestId('Not found page')).toBeInTheDocument()
@@ -68,38 +66,24 @@ describe('', () => {
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/tcp/routers/tcp-all@docker', withPage: true },
)
const routerStructure = getByTestId('router-structure')
expect(routerStructure.innerHTML).toContain(':443')
expect(routerStructure.innerHTML).toContain(':8000')
- expect(routerStructure.innerHTML).toContain('tcp-all@docker')
- expect(routerStructure.innerHTML).toContain('tcp-all')
expect(routerStructure.innerHTML).toContain('TCP Router')
expect(routerStructure.innerHTML).not.toContain('HTTP Router')
const routerDetailsSection = getByTestId('router-details')
- const routerDetailsPanel = routerDetailsSection.querySelector(':scope > div:nth-child(1)')
- expect(routerDetailsPanel?.innerHTML).toContain('Status')
- expect(routerDetailsPanel?.innerHTML).toContain('Success')
- expect(routerDetailsPanel?.innerHTML).toContain('Provider')
- expect(routerDetailsPanel?.querySelector('svg[data-testid="docker"]')).toBeTruthy()
- expect(routerDetailsPanel?.innerHTML).toContain('Name')
- expect(routerDetailsPanel?.innerHTML).toContain('tcp-all@docker')
- expect(routerDetailsPanel?.innerHTML).toContain('Entrypoints')
- expect(routerDetailsPanel?.innerHTML).toContain('web')
- expect(routerDetailsPanel?.innerHTML).toContain('web-secured')
- expect(routerDetailsPanel?.innerHTML).toContain('tcp-all')
-
- const middlewaresPanel = routerDetailsSection.querySelector(':scope > div:nth-child(3)')
- const providers = Array.from(middlewaresPanel?.querySelectorAll('svg[data-testid="docker"]') || [])
- expect(middlewaresPanel?.innerHTML).toContain('middleware00')
- expect(middlewaresPanel?.innerHTML).toContain('middleware01')
- expect(middlewaresPanel?.innerHTML).toContain('Success')
- expect(providers.length).toBe(2)
+ expect(routerDetailsSection?.innerHTML).toContain('Status')
+ expect(routerDetailsSection?.innerHTML).toContain('Success')
+ expect(routerDetailsSection?.innerHTML).toContain('Provider')
+ expect(routerDetailsSection?.querySelector('svg[data-testid="docker"]')).toBeTruthy()
+ expect(routerStructure.innerHTML).toContain('middleware00')
+ expect(routerStructure.innerHTML).toContain('middleware01')
expect(getByTestId('/tcp/services/tcp-all@docker')).toBeInTheDocument()
})
diff --git a/webui/src/pages/tcp/TcpRouter.tsx b/webui/src/pages/tcp/TcpRouter.tsx
index 92f9b47b2..b0bfca0cc 100644
--- a/webui/src/pages/tcp/TcpRouter.tsx
+++ b/webui/src/pages/tcp/TcpRouter.tsx
@@ -1,91 +1,13 @@
-import { Flex, styled, Text } from '@traefiklabs/faency'
-import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
-import { CardListSection, DetailSectionSkeleton } from 'components/resources/DetailSections'
-import MiddlewarePanel from 'components/resources/MiddlewarePanel'
-import RouterPanel from 'components/resources/RouterPanel'
-import TlsPanel from 'components/resources/TlsPanel'
-import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail'
-import { RouterStructure } from 'pages/http/HttpRouter'
-import { NotFound } from 'pages/NotFound'
-
-type DetailProps = {
- data: ResourceDetailDataType
-}
-
-const SpacedColumns = styled(Flex, {
- display: 'grid',
- gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
- gridGap: '16px',
-})
-
-const RouterDetail = ({ data }: DetailProps) => (
-
-
-
-
-
-)
-
-type TcpRouterRenderProps = {
- data?: ResourceDetailDataType
- error?: Error
- name: string
-}
-
-export const TcpRouterRender = ({ data, error, name }: TcpRouterRenderProps) => {
- if (error) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
- Sorry, we could not fetch detail information for this Router right now. Please, try again later.
-
- >
- )
- }
-
- if (!data) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
-
-
-
-
-
-
-
-
- >
- )
- }
-
- if (!data.name) {
- return
- }
-
- return (
- <>
-
- {data.name} - Traefik Proxy
-
-
-
- >
- )
-}
+import { RouterDetail } from 'components/routers/RouterDetail'
+import { useResourceDetail } from 'hooks/use-resource-detail'
export const TcpRouter = () => {
const { name } = useParams<{ name: string }>()
const { data, error } = useResourceDetail(name!, 'routers', 'tcp')
- return
+
+ return
}
export default TcpRouter
diff --git a/webui/src/pages/tcp/TcpRouters.tsx b/webui/src/pages/tcp/TcpRouters.tsx
index 8a8f638ac..12f77dddc 100644
--- a/webui/src/pages/tcp/TcpRouters.tsx
+++ b/webui/src/pages/tcp/TcpRouters.tsx
@@ -1,18 +1,18 @@
import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency'
import { useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
-import { FiShield } from 'react-icons/fi'
import useInfiniteScroll from 'react-infinite-scroll-hook'
import { useSearchParams } from 'react-router-dom'
-import ClickableRow from 'components/ClickableRow'
-import ProviderIcon from 'components/icons/providers'
-import { Chips } from 'components/resources/DetailSections'
+import { ScrollTopButton } from 'components/buttons/ScrollTopButton'
+import { ProviderIconWithTooltip } from 'components/icons/providers'
+import { Chips } from 'components/resources/DetailItemComponents'
import { ResourceStatus } from 'components/resources/ResourceStatus'
-import { ScrollTopButton } from 'components/ScrollTopButton'
+import TlsIcon from 'components/routers/TlsIcon'
import { SpinnerLoader } from 'components/SpinnerLoader'
-import { searchParamsToState, TableFilter } from 'components/TableFilter'
+import ClickableRow from 'components/tables/ClickableRow'
import SortableTh from 'components/tables/SortableTh'
+import { searchParamsToState, TableFilter } from 'components/tables/TableFilter'
import Tooltip from 'components/Tooltip'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
@@ -22,17 +22,13 @@ export const makeRowRender = (): RenderRowType => {
const TcpRoutersRenderRow = (row) => (
-
-
-
-
-
+
{row.tls && (
-
+
)}
@@ -48,11 +44,7 @@ export const makeRowRender = (): RenderRowType => {
-
-
-
-
-
+
@@ -82,7 +74,7 @@ export const TcpRoutersRender = ({
-
+
diff --git a/webui/src/pages/tcp/TcpService.spec.tsx b/webui/src/pages/tcp/TcpService.spec.tsx
index faa8ac0f5..ea001e457 100644
--- a/webui/src/pages/tcp/TcpService.spec.tsx
+++ b/webui/src/pages/tcp/TcpService.spec.tsx
@@ -1,12 +1,10 @@
-import { TcpServiceRender } from './TcpService'
-
-import { ResourceDetailDataType } from 'hooks/use-resource-detail'
+import { ServiceDetail } from 'components/services/ServiceDetail'
import { renderWithProviders } from 'utils/test'
describe('', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/tcp/services/mock-service', withPage: true },
)
expect(getByTestId('error-text')).toBeInTheDocument()
@@ -14,7 +12,7 @@ describe('', () => {
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/tcp/services/mock-service', withPage: true },
)
expect(getByTestId('skeleton')).toBeInTheDocument()
@@ -22,7 +20,7 @@ describe('', () => {
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/tcp/services/mock-service', withPage: true },
)
expect(getByTestId('Not found page')).toBeInTheDocument()
@@ -71,7 +69,7 @@ describe('', () => {
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/tcp/services/mock-service', withPage: true },
)
@@ -79,38 +77,37 @@ describe('', () => {
const titleTags = headings.filter((h1) => h1.innerHTML === 'service-test1')
expect(titleTags.length).toBe(1)
- const serviceDetails = getByTestId('tcp-service-details')
+ const serviceDetails = getByTestId('service-details')
expect(serviceDetails.innerHTML).toContain('Type')
expect(serviceDetails.innerHTML).toContain('loadbalancer')
expect(serviceDetails.innerHTML).toContain('Provider')
expect(serviceDetails.querySelector('svg[data-testid="docker"]')).toBeTruthy()
expect(serviceDetails.innerHTML).toContain('Status')
expect(serviceDetails.innerHTML).toContain('Success')
- expect(serviceDetails.innerHTML).toContain('Termination Delay')
+ expect(serviceDetails.innerHTML).toContain('Termination delay')
expect(serviceDetails.innerHTML).toContain('10 ms')
- const healthCheck = getByTestId('tcp-health-check')
+ const healthCheck = getByTestId('health-check')
expect(healthCheck.innerHTML).toContain('Interval')
expect(healthCheck.innerHTML).toContain('30s')
expect(healthCheck.innerHTML).toContain('Timeout')
expect(healthCheck.innerHTML).toContain('10s')
expect(healthCheck.innerHTML).toContain('Port')
expect(healthCheck.innerHTML).toContain('8080')
- expect(healthCheck.innerHTML).toContain('Unhealthy Interval')
+ expect(healthCheck.innerHTML).toContain('Unhealthy interval')
expect(healthCheck.innerHTML).toContain('1m')
expect(healthCheck.innerHTML).toContain('Send')
expect(healthCheck.innerHTML).toContain('PING')
expect(healthCheck.innerHTML).toContain('Expect')
expect(healthCheck.innerHTML).toContain('PONG')
- const serversList = getByTestId('tcp-servers-list')
+ const serversList = getByTestId('servers-list')
expect(serversList.childNodes.length).toBe(1)
expect(serversList.innerHTML).toContain('http://10.0.1.12:80')
const routersTable = getByTestId('routers-table')
- const tableBody = routersTable.querySelectorAll('div[role="rowgroup"]')[1]
- expect(tableBody?.querySelectorAll('a[role="row"]')).toHaveLength(1)
- expect(tableBody?.innerHTML).toContain('router-test1@docker')
+ expect(routersTable.querySelectorAll('a[role="row"]')).toHaveLength(1)
+ expect(routersTable.innerHTML).toContain('router-test1@docker')
})
it('should render the service servers from the serverStatus property', async () => {
@@ -153,19 +150,18 @@ describe('', () => {
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/tcp/services/mock-service', withPage: true },
)
- const serversList = getByTestId('tcp-servers-list')
+ const serversList = getByTestId('servers-list')
expect(serversList.childNodes.length).toBe(1)
expect(serversList.innerHTML).toContain('http://10.0.1.12:81')
const routersTable = getByTestId('routers-table')
- const tableBody = routersTable.querySelectorAll('div[role="rowgroup"]')[1]
- expect(tableBody?.querySelectorAll('a[role="row"]')).toHaveLength(2)
- expect(tableBody?.innerHTML).toContain('router-test1@docker')
- expect(tableBody?.innerHTML).toContain('router-test2@docker')
+ expect(routersTable.querySelectorAll('a[role="row"]')).toHaveLength(2)
+ expect(routersTable.innerHTML).toContain('router-test1@docker')
+ expect(routersTable.innerHTML).toContain('router-test2@docker')
})
it('should not render used by routers table if the usedBy property is empty', async () => {
@@ -180,7 +176,7 @@ describe('', () => {
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/tcp/services/mock-service', withPage: true },
)
@@ -223,14 +219,14 @@ describe('', () => {
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
)
const headings = Array.from(container.getElementsByTagName('h1'))
const titleTags = headings.filter((h1) => h1.innerHTML === 'weighted-service-test')
expect(titleTags.length).toBe(1)
- const serviceDetails = getByTestId('tcp-service-details')
+ const serviceDetails = getByTestId('service-details')
expect(serviceDetails.innerHTML).toContain('Type')
expect(serviceDetails.innerHTML).toContain('weighted')
expect(serviceDetails.innerHTML).toContain('Provider')
@@ -238,7 +234,7 @@ describe('', () => {
expect(serviceDetails.innerHTML).toContain('Status')
expect(serviceDetails.innerHTML).toContain('Success')
- const weightedServices = getByTestId('tcp-weighted-services')
+ const weightedServices = getByTestId('weighted-services')
expect(weightedServices.childNodes.length).toBe(2)
expect(weightedServices.innerHTML).toContain('service1@docker')
expect(weightedServices.innerHTML).toContain('80')
diff --git a/webui/src/pages/tcp/TcpService.tsx b/webui/src/pages/tcp/TcpService.tsx
index c36106156..671222f3e 100644
--- a/webui/src/pages/tcp/TcpService.tsx
+++ b/webui/src/pages/tcp/TcpService.tsx
@@ -1,291 +1,13 @@
-import { Box, Flex, H1, Skeleton, styled, Text } from '@traefiklabs/faency'
-import { useMemo } from 'react'
-import { Helmet } from 'react-helmet-async'
-import { FiGlobe, FiInfo, FiShield } from 'react-icons/fi'
import { useParams } from 'react-router-dom'
-import ProviderIcon from 'components/icons/providers'
-import {
- DetailSection,
- DetailSectionSkeleton,
- ItemBlock,
- ItemTitle,
- LayoutTwoCols,
- ProviderName,
-} from 'components/resources/DetailSections'
-import { ResourceStatus } from 'components/resources/ResourceStatus'
-import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection'
-import Tooltip from 'components/Tooltip'
-import { ResourceDetailDataType, ServiceDetailType, useResourceDetail } from 'hooks/use-resource-detail'
-import { NotFound } from 'pages/NotFound'
-
-type TcpDetailProps = {
- data: ServiceDetailType
-}
-
-const SpacedColumns = styled(Flex, {
- display: 'grid',
- gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
- gridGap: '16px',
-})
-
-const ServicesGrid = styled(Box, {
- display: 'grid',
- gridTemplateColumns: '2fr 1fr 1fr',
- alignItems: 'center',
- padding: '$3 $5',
- borderBottom: '1px solid $tableRowBorder',
-})
-
-const ServersGrid = styled(Box, {
- display: 'grid',
- alignItems: 'center',
- padding: '$3 $5',
- borderBottom: '1px solid $tableRowBorder',
-})
-
-const GridTitle = styled(Text, {
- fontSize: '14px',
- fontWeight: 700,
- color: 'hsl(0, 0%, 56%)',
-})
-
-type TcpServer = {
- address: string
-}
-
-type ServerStatus = {
- [server: string]: string
-}
-
-type TcpHealthCheck = {
- port?: number
- send?: string
- expect?: string
- interval?: string
- unhealthyInterval?: string
- timeout?: string
-}
-
-function getTcpServerStatusList(data: ServiceDetailType): ServerStatus {
- const serversList: ServerStatus = {}
-
- data.loadBalancer?.servers?.forEach((server: any) => {
- // TCP servers should have address, but handle both url and address for compatibility
- const serverKey = (server as TcpServer).address || (server as any).url
- if (serverKey) {
- serversList[serverKey] = 'DOWN'
- }
- })
-
- if (data.serverStatus) {
- Object.entries(data.serverStatus).forEach(([server, status]) => {
- serversList[server] = status
- })
- }
-
- return serversList
-}
-
-export const TcpServicePanels = ({ data }: TcpDetailProps) => {
- const serversList = getTcpServerStatusList(data)
- const getProviderFromName = (serviceName: string): string => {
- const [, provider] = serviceName.split('@')
- return provider || data.provider
- }
- const providerName = useMemo(() => {
- return data.provider
- }, [data.provider])
-
- return (
-
- } title="Service Details">
-
- {data.type && (
-
- {data.type}
-
- )}
- {data.provider && (
-
-
- {providerName}
-
- )}
-
- {data.status && (
-
-
-
- )}
- {data.loadBalancer && (
- <>
- {data.loadBalancer.terminationDelay && (
-
- {`${data.loadBalancer.terminationDelay} ms`}
-
- )}
- >
- )}
-
- {data.loadBalancer?.healthCheck && (
- } title="Health Check">
-
- {(() => {
- const tcpHealthCheck = data.loadBalancer.healthCheck as unknown as TcpHealthCheck
- return (
- <>
-
- {tcpHealthCheck.interval && (
-
- {tcpHealthCheck.interval}
-
- )}
- {tcpHealthCheck.timeout && (
-
- {tcpHealthCheck.timeout}
-
- )}
-
-
- {tcpHealthCheck.port && (
-
- {tcpHealthCheck.port}
-
- )}
- {tcpHealthCheck.unhealthyInterval && (
-
- {tcpHealthCheck.unhealthyInterval}
-
- )}
-
-
- {tcpHealthCheck.send && (
-
-
- {tcpHealthCheck.send}
-
-
- )}
- {tcpHealthCheck.expect && (
-
-
- {tcpHealthCheck.expect}
-
-
- )}
-
- >
- )
- })()}
-
-
- )}
- {!!data?.weighted?.services?.length && (
- } title="Services" noPadding>
- <>
-
- Name
- Weight
- Provider
-
-
- {data.weighted.services.map((service) => (
-
- {service.name}
- {service.weight}
-
-
-
-
- ))}
-
- >
-
- )}
- {Object.keys(serversList).length > 0 && (
- } title="Servers" noPadding>
- <>
-
- Status
- Address
-
-
- {Object.entries(serversList).map(([server, status]) => (
-
-
-
-
- {server}
-
-
-
- ))}
-
- >
-
- )}
-
- )
-}
-
-type TcpServiceRenderProps = {
- data?: ResourceDetailDataType
- error?: Error
- name: string
-}
-
-export const TcpServiceRender = ({ data, error, name }: TcpServiceRenderProps) => {
- if (error) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
- Sorry, we could not fetch detail information for this Service right now. Please, try again later.
-
- >
- )
- }
-
- if (!data) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
-
-
-
-
-
-
- >
- )
- }
-
- if (!data.name) {
- return
- }
-
- return (
- <>
-
- {data.name} - Traefik Proxy
-
- {data.name}
-
-
- >
- )
-}
+import { ServiceDetail } from 'components/services/ServiceDetail'
+import { useResourceDetail } from 'hooks/use-resource-detail'
export const TcpService = () => {
const { name } = useParams<{ name: string }>()
const { data, error } = useResourceDetail(name!, 'services', 'tcp')
- return
+
+ return
}
export default TcpService
diff --git a/webui/src/pages/tcp/TcpServices.tsx b/webui/src/pages/tcp/TcpServices.tsx
index 13df8792b..f02a3160b 100644
--- a/webui/src/pages/tcp/TcpServices.tsx
+++ b/webui/src/pages/tcp/TcpServices.tsx
@@ -1,17 +1,16 @@
-import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex, Text } from '@traefiklabs/faency'
+import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Flex, Text } from '@traefiklabs/faency'
import { useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
import useInfiniteScroll from 'react-infinite-scroll-hook'
import { useSearchParams } from 'react-router-dom'
-import ClickableRow from 'components/ClickableRow'
-import ProviderIcon from 'components/icons/providers'
+import { ScrollTopButton } from 'components/buttons/ScrollTopButton'
+import { ProviderIconWithTooltip } from 'components/icons/providers'
import { ResourceStatus } from 'components/resources/ResourceStatus'
-import { ScrollTopButton } from 'components/ScrollTopButton'
import { SpinnerLoader } from 'components/SpinnerLoader'
-import { searchParamsToState, TableFilter } from 'components/TableFilter'
+import ClickableRow from 'components/tables/ClickableRow'
import SortableTh from 'components/tables/SortableTh'
-import Tooltip from 'components/Tooltip'
+import { searchParamsToState, TableFilter } from 'components/tables/TableFilter'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
@@ -20,11 +19,7 @@ export const makeRowRender = (): RenderRowType => {
const TcpServicesRenderRow = (row) => (
-
-
-
-
-
+
@@ -36,11 +31,7 @@ export const makeRowRender = (): RenderRowType => {
{row.loadBalancer?.servers?.length || 0}
-
-
-
-
-
+
)
@@ -67,7 +58,7 @@ export const TcpServicesRender = ({
-
+
diff --git a/webui/src/pages/udp/UdpRouter.spec.tsx b/webui/src/pages/udp/UdpRouter.spec.tsx
index 2404b70a3..23f80d663 100644
--- a/webui/src/pages/udp/UdpRouter.spec.tsx
+++ b/webui/src/pages/udp/UdpRouter.spec.tsx
@@ -1,12 +1,10 @@
-import { UdpRouterRender } from './UdpRouter'
-
-import { ResourceDetailDataType } from 'hooks/use-resource-detail'
+import { RouterDetail } from 'components/routers/RouterDetail'
import { renderWithProviders } from 'utils/test'
describe('', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/udp/routers/mock-router', withPage: true },
)
expect(getByTestId('error-text')).toBeInTheDocument()
@@ -14,7 +12,7 @@ describe('', () => {
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/udp/routers/mock-router', withPage: true },
)
expect(getByTestId('skeleton')).toBeInTheDocument()
@@ -22,7 +20,7 @@ describe('', () => {
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/udp/routers/mock-router', withPage: true },
)
expect(getByTestId('Not found page')).toBeInTheDocument()
@@ -53,31 +51,22 @@ describe('', () => {
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/udp/routers/udp-all@docker', withPage: true },
)
const routerStructure = getByTestId('router-structure')
expect(routerStructure.innerHTML).toContain(':443')
expect(routerStructure.innerHTML).toContain(':8000')
- expect(routerStructure.innerHTML).toContain('udp-all@docker')
- expect(routerStructure.innerHTML).toContain('udp-all')
expect(routerStructure.innerHTML).toContain('UDP Router')
expect(routerStructure.innerHTML).not.toContain('HTTP Router')
const routerDetailsSection = getByTestId('router-details')
- const routerDetailsPanel = routerDetailsSection.querySelector(':scope > div:nth-child(1)')
- expect(routerDetailsPanel?.innerHTML).toContain('Status')
- expect(routerDetailsPanel?.innerHTML).toContain('Success')
- expect(routerDetailsPanel?.innerHTML).toContain('Provider')
- expect(routerDetailsPanel?.querySelector('svg[data-testid="docker"]')).toBeTruthy()
- expect(routerDetailsPanel?.innerHTML).toContain('Name')
- expect(routerDetailsPanel?.innerHTML).toContain('udp-all@docker')
- expect(routerDetailsPanel?.innerHTML).toContain('Entrypoints')
- expect(routerDetailsPanel?.innerHTML).toContain('web')
- expect(routerDetailsPanel?.innerHTML).toContain('web-secured')
- expect(routerDetailsPanel?.innerHTML).toContain('udp-all')
+ expect(routerDetailsSection?.innerHTML).toContain('Status')
+ expect(routerDetailsSection?.innerHTML).toContain('Success')
+ expect(routerDetailsSection?.innerHTML).toContain('Provider')
+ expect(routerDetailsSection?.querySelector('svg[data-testid="docker"]')).toBeTruthy()
expect(getByTestId('/udp/services/udp-all@docker')).toBeInTheDocument()
})
diff --git a/webui/src/pages/udp/UdpRouter.tsx b/webui/src/pages/udp/UdpRouter.tsx
index a41bdb0df..a864da583 100644
--- a/webui/src/pages/udp/UdpRouter.tsx
+++ b/webui/src/pages/udp/UdpRouter.tsx
@@ -1,88 +1,13 @@
-import { Flex, styled, Text } from '@traefiklabs/faency'
-import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
-import { CardListSection, DetailSectionSkeleton } from 'components/resources/DetailSections'
-import RouterPanel from 'components/resources/RouterPanel'
-import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail'
-import { RouterStructure } from 'pages/http/HttpRouter'
-import { NotFound } from 'pages/NotFound'
-
-type DetailProps = {
- data: ResourceDetailDataType
-}
-
-const SpacedColumns = styled(Flex, {
- display: 'grid',
- gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
- gridGap: '16px',
-})
-
-const RouterDetail = ({ data }: DetailProps) => (
-
-
-
-)
-
-type UdpRouterRenderProps = {
- data?: ResourceDetailDataType
- error?: Error
- name: string
-}
-
-export const UdpRouterRender = ({ data, error, name }: UdpRouterRenderProps) => {
- if (error) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
- Sorry, we could not fetch detail information for this Router right now. Please, try again later.
-
- >
- )
- }
-
- if (!data) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
-
-
-
-
-
-
-
-
- >
- )
- }
-
- if (!data.name) {
- return
- }
-
- return (
- <>
-
- {data.name} - Traefik Proxy
-
-
-
- >
- )
-}
+import { RouterDetail } from 'components/routers/RouterDetail'
+import { useResourceDetail } from 'hooks/use-resource-detail'
export const UdpRouter = () => {
const { name } = useParams<{ name: string }>()
const { data, error } = useResourceDetail(name!, 'routers', 'udp')
- return
+ return
}
export default UdpRouter
diff --git a/webui/src/pages/udp/UdpRouters.tsx b/webui/src/pages/udp/UdpRouters.tsx
index b468630ce..880b9c5d3 100644
--- a/webui/src/pages/udp/UdpRouters.tsx
+++ b/webui/src/pages/udp/UdpRouters.tsx
@@ -1,18 +1,17 @@
-import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency'
+import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Flex } from '@traefiklabs/faency'
import { useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
import useInfiniteScroll from 'react-infinite-scroll-hook'
import { useSearchParams } from 'react-router-dom'
-import ClickableRow from 'components/ClickableRow'
-import ProviderIcon from 'components/icons/providers'
-import { Chips } from 'components/resources/DetailSections'
+import { ScrollTopButton } from 'components/buttons/ScrollTopButton'
+import { ProviderIconWithTooltip } from 'components/icons/providers'
+import { Chips } from 'components/resources/DetailItemComponents'
import { ResourceStatus } from 'components/resources/ResourceStatus'
-import { ScrollTopButton } from 'components/ScrollTopButton'
import { SpinnerLoader } from 'components/SpinnerLoader'
-import { searchParamsToState, TableFilter } from 'components/TableFilter'
+import ClickableRow from 'components/tables/ClickableRow'
import SortableTh from 'components/tables/SortableTh'
-import Tooltip from 'components/Tooltip'
+import { searchParamsToState, TableFilter } from 'components/tables/TableFilter'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
@@ -21,11 +20,7 @@ export const makeRowRender = (): RenderRowType => {
const UdpRoutersRenderRow = (row) => (
-
-
-
-
-
+
{row.entryPoints && row.entryPoints.length > 0 && }
@@ -35,11 +30,7 @@ export const makeRowRender = (): RenderRowType => {
-
-
-
-
-
+
@@ -69,7 +60,7 @@ export const UdpRoutersRender = ({
-
+
diff --git a/webui/src/pages/udp/UdpService.spec.tsx b/webui/src/pages/udp/UdpService.spec.tsx
index b6150c8ee..6f3dd1c47 100644
--- a/webui/src/pages/udp/UdpService.spec.tsx
+++ b/webui/src/pages/udp/UdpService.spec.tsx
@@ -1,12 +1,10 @@
-import { UdpServiceRender } from './UdpService'
-
-import { ResourceDetailDataType } from 'hooks/use-resource-detail'
+import { ServiceDetail } from 'components/services/ServiceDetail'
import { renderWithProviders } from 'utils/test'
describe('', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/udp/services/mock-service', withPage: true },
)
expect(getByTestId('error-text')).toBeInTheDocument()
@@ -14,7 +12,7 @@ describe('', () => {
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/udp/services/mock-service', withPage: true },
)
expect(getByTestId('skeleton')).toBeInTheDocument()
@@ -22,7 +20,7 @@ describe('', () => {
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
- ,
+ ,
{ route: '/udp/services/mock-service', withPage: true },
)
expect(getByTestId('Not found page')).toBeInTheDocument()
@@ -61,7 +59,7 @@ describe('', () => {
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/udp/services/mock-service', withPage: true },
)
@@ -76,9 +74,9 @@ describe('', () => {
expect(serviceDetails.querySelector('svg[data-testid="docker"]')).toBeTruthy()
expect(serviceDetails.innerHTML).toContain('Status')
expect(serviceDetails.innerHTML).toContain('Success')
- expect(serviceDetails.innerHTML).toContain('Pass Host Header')
+ expect(serviceDetails.innerHTML).toContain('Pass host header')
expect(serviceDetails.innerHTML).toContain('True')
- expect(serviceDetails.innerHTML).toContain('Termination Delay')
+ expect(serviceDetails.innerHTML).toContain('Termination delay')
expect(serviceDetails.innerHTML).toContain('10 ms')
const serversList = getByTestId('servers-list')
@@ -86,9 +84,8 @@ describe('', () => {
expect(serversList.innerHTML).toContain('http://10.0.1.12:80')
const routersTable = getByTestId('routers-table')
- const tableBody = routersTable.querySelectorAll('div[role="rowgroup"]')[1]
- expect(tableBody?.querySelectorAll('a[role="row"]')).toHaveLength(1)
- expect(tableBody?.innerHTML).toContain('router-test1@docker')
+ expect(routersTable.querySelectorAll('a[role="row"]')).toHaveLength(1)
+ expect(routersTable.innerHTML).toContain('router-test1@docker')
})
it('should render the service servers from the serverStatus property', async () => {
@@ -131,7 +128,7 @@ describe('', () => {
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/udp/services/mock-service', withPage: true },
)
@@ -140,10 +137,9 @@ describe('', () => {
expect(serversList.innerHTML).toContain('http://10.0.1.12:81')
const routersTable = getByTestId('routers-table')
- const tableBody = routersTable.querySelectorAll('div[role="rowgroup"]')[1]
- expect(tableBody?.querySelectorAll('a[role="row"]')).toHaveLength(2)
- expect(tableBody?.innerHTML).toContain('router-test1@docker')
- expect(tableBody?.innerHTML).toContain('router-test2@docker')
+ expect(routersTable.querySelectorAll('a[role="row"]')).toHaveLength(2)
+ expect(routersTable.innerHTML).toContain('router-test1@docker')
+ expect(routersTable.innerHTML).toContain('router-test2@docker')
})
it('should not render used by routers table if the usedBy property is empty', async () => {
@@ -158,7 +154,7 @@ describe('', () => {
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- ,
+ ,
{ route: '/udp/services/mock-service', withPage: true },
)
diff --git a/webui/src/pages/udp/UdpService.tsx b/webui/src/pages/udp/UdpService.tsx
index 132e63197..6ba25fd2f 100644
--- a/webui/src/pages/udp/UdpService.tsx
+++ b/webui/src/pages/udp/UdpService.tsx
@@ -1,75 +1,13 @@
-import { Flex, H1, Skeleton, styled, Text } from '@traefiklabs/faency'
-import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
-import { DetailSectionSkeleton } from 'components/resources/DetailSections'
-import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection'
-import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail'
-import { ServicePanels } from 'pages/http/HttpService'
-import { NotFound } from 'pages/NotFound'
-
-const SpacedColumns = styled(Flex, {
- display: 'grid',
- gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
- gridGap: '16px',
-})
-
-type UdpServiceRenderProps = {
- data?: ResourceDetailDataType
- error?: Error
- name: string
-}
-
-export const UdpServiceRender = ({ data, error, name }: UdpServiceRenderProps) => {
- if (error) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
- Sorry, we could not fetch detail information for this Service right now. Please, try again later.
-
- >
- )
- }
-
- if (!data) {
- return (
- <>
-
- {name} - Traefik Proxy
-
-
-
-
-
-
-
- >
- )
- }
-
- if (!data.name) {
- return
- }
-
- return (
- <>
-
- {data.name} - Traefik Proxy
-
- {data.name}
-
-
- >
- )
-}
+import { ServiceDetail } from 'components/services/ServiceDetail'
+import { useResourceDetail } from 'hooks/use-resource-detail'
export const UdpService = () => {
const { name } = useParams<{ name: string }>()
const { data, error } = useResourceDetail(name!, 'services', 'udp')
- return
+
+ return
}
export default UdpService
diff --git a/webui/src/pages/udp/UdpServices.tsx b/webui/src/pages/udp/UdpServices.tsx
index 76abc3d02..c87be266a 100644
--- a/webui/src/pages/udp/UdpServices.tsx
+++ b/webui/src/pages/udp/UdpServices.tsx
@@ -1,17 +1,16 @@
-import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex, Text } from '@traefiklabs/faency'
+import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Flex, Text } from '@traefiklabs/faency'
import { useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
import useInfiniteScroll from 'react-infinite-scroll-hook'
import { useSearchParams } from 'react-router-dom'
-import ClickableRow from 'components/ClickableRow'
-import ProviderIcon from 'components/icons/providers'
+import { ScrollTopButton } from 'components/buttons/ScrollTopButton'
+import { ProviderIconWithTooltip } from 'components/icons/providers'
import { ResourceStatus } from 'components/resources/ResourceStatus'
-import { ScrollTopButton } from 'components/ScrollTopButton'
import { SpinnerLoader } from 'components/SpinnerLoader'
-import { searchParamsToState, TableFilter } from 'components/TableFilter'
+import ClickableRow from 'components/tables/ClickableRow'
import SortableTh from 'components/tables/SortableTh'
-import Tooltip from 'components/Tooltip'
+import { searchParamsToState, TableFilter } from 'components/tables/TableFilter'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
@@ -20,11 +19,7 @@ export const makeRowRender = (): RenderRowType => {
const UdpServicesRenderRow = (row) => (
-
-
-
-
-
+
@@ -36,11 +31,7 @@ export const makeRowRender = (): RenderRowType => {
{row.loadBalancer?.servers?.length || 0}
-
-
-
-
-
+
)
@@ -67,7 +58,7 @@ export const UdpServicesRender = ({
-
+
diff --git a/webui/src/types/object.d.ts b/webui/src/types/object.d.ts
new file mode 100644
index 000000000..9321ca555
--- /dev/null
+++ b/webui/src/types/object.d.ts
@@ -0,0 +1,9 @@
+declare namespace Object {
+ type JSONObject = {
+ [x: string]: string | number
+ }
+
+ type ValuesMapType = {
+ [key: string]: string | number | JSONObject
+ }
+}
diff --git a/webui/src/types/resources.d.ts b/webui/src/types/resources.d.ts
new file mode 100644
index 000000000..7bed033e0
--- /dev/null
+++ b/webui/src/types/resources.d.ts
@@ -0,0 +1,123 @@
+declare namespace Resource {
+ type Status = 'info' | 'success' | 'warning' | 'error' | 'enabled' | 'disabled' | 'loading'
+
+ type DetailsData = Router.DetailsData & Service.Details & Middleware.DetailsData
+}
+
+declare namespace Entrypoint {
+ type Details = {
+ name: string
+ address: string
+ message?: string
+ }
+}
+
+declare namespace Router {
+ type TlsDomain = {
+ main: string
+ sans: string[]
+ }
+
+ type TLS = {
+ options: string
+ certResolver: string
+ domains: TlsDomain[]
+ passthrough: boolean
+ }
+
+ type Details = {
+ name: string
+ service?: string
+ status: 'enabled' | 'disabled' | 'warning'
+ rule?: string
+ priority?: number
+ provider: string
+ tls?: {
+ options: string
+ certResolver: string
+ domains: TlsDomain[]
+ passthrough: boolean
+ }
+ error?: string[]
+ entryPoints?: string[]
+ message?: string
+ }
+
+ type DetailsData = Details & {
+ middlewares?: Middleware.Details[]
+ hasValidMiddlewares?: boolean
+ entryPointsData?: Entrypoint.Details[]
+ using?: string[]
+ }
+}
+
+declare namespace Service {
+ type WeightedService = {
+ name: string
+ weight: number
+ }
+
+ type Mirror = {
+ name: string
+ percent: number
+ }
+
+ type Details = {
+ name: string
+ status: 'enabled' | 'disabled' | 'warning'
+ provider: string
+ type: string
+ usedBy?: string[]
+ routers?: Router[]
+ serverStatus?: {
+ [server: string]: string
+ }
+ mirroring?: {
+ service: string
+ mirrors?: Mirror[]
+ }
+ loadBalancer?: {
+ servers?: { url: string }[]
+ passHostHeader?: boolean
+ terminationDelay?: number
+ healthCheck?: {
+ scheme: string
+ path: string
+ hostname: string
+ headers?: {
+ [header: string]: string
+ }
+ port?: number
+ send?: string
+ expect?: string
+ interval?: string
+ unhealthyInterval?: string
+ timeout?: string
+ }
+ }
+ weighted?: {
+ services?: WeightedService[]
+ }
+ }
+}
+
+declare namespace Middleware {
+ type Props = {
+ [prop: string]: ValuesMapType
+ }
+
+ type Details = {
+ name: string
+ status: 'enabled' | 'disabled' | 'warning'
+ provider: string
+ type?: string
+ plugin?: Record
+ error?: string[]
+ routers?: string[]
+ usedBy?: string[]
+ } & Props
+
+ type DetailsData = Details & {
+ routers?: Router.Details[]
+ }
+}