1
0
Fork 0

Migrate Traefik Proxy dashboard UI to React

This commit is contained in:
Gina A. 2025-05-28 11:26:04 +02:00 committed by GitHub
parent 4790e4910f
commit f16fff577a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
324 changed files with 28303 additions and 19567 deletions

View file

@ -1,109 +0,0 @@
<template>
<page-default>
<section class="app-section">
<div class="app-section-wrap app-boxed app-boxed-xl q-pl-md q-pr-md q-pt-xl q-pb-xl">
<div class="row no-wrap items-center q-mb-lg">
<tool-bar-table
v-model:status="status"
v-model:filter="filter"
/>
</div>
<div class="row items-center q-col-gutter-lg">
<div class="col-12">
<main-table
ref="mainTable"
v-bind="getTableProps({ type: 'tcp-middlewares' })"
v-model:current-sort="sortBy"
v-model:current-sort-dir="sortDir"
:data="allMiddlewares.items"
:on-load-more="handleLoadMore"
:end-reached="allMiddlewares.endReached"
:loading="allMiddlewares.loading"
/>
</div>
</div>
</div>
</section>
</page-default>
</template>
<script>
import { defineComponent } from 'vue'
import { mapActions, mapGetters } from 'vuex'
import GetTablePropsMixin from '../../_mixins/GetTableProps'
import PaginationMixin from '../../_mixins/Pagination'
import PageDefault from '../../components/_commons/PageDefault.vue'
import ToolBarTable from '../../components/_commons/ToolBarTable.vue'
import MainTable from '../../components/_commons/MainTable.vue'
export default defineComponent({
name: 'PageTCPMiddlewares',
components: {
PageDefault,
ToolBarTable,
MainTable
},
mixins: [
GetTablePropsMixin,
PaginationMixin({
fetchMethod: 'getAllMiddlewaresWithParams',
scrollerRef: 'mainTable.$refs.scroller',
pollingIntervalTime: 5000
})
],
data () {
return {
filter: '',
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
...mapGetters('tcp', { allMiddlewares: 'allMiddlewares' })
},
watch: {
'status' () {
this.refreshAll()
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeUnmount () {
this.$store.commit('tcp/getAllMiddlewaresClear')
},
methods: {
...mapActions('tcp', { getAllMiddlewares: 'getAllMiddlewares' }),
getAllMiddlewaresWithParams (params) {
return this.getAllMiddlewares({
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
...params
})
},
refreshAll () {
if (this.allMiddlewares.loading) {
return
}
this.initFetch()
},
handleLoadMore ({ page = 1 } = {}) {
return this.fetchMore({ page })
}
}
})
</script>
<style scoped lang="scss">
</style>

View file

@ -1,111 +0,0 @@
<template>
<page-default>
<section class="app-section">
<div class="app-section-wrap app-boxed app-boxed-xl q-pl-md q-pr-md q-pt-xl q-pb-xl">
<div class="row no-wrap items-center q-mb-lg">
<tool-bar-table
v-model:status="status"
v-model:filter="filter"
/>
</div>
<div class="row items-center q-col-gutter-lg">
<div class="col-12">
<main-table
ref="mainTable"
v-bind="getTableProps({ type: 'tcp-routers' })"
v-model:current-sort="sortBy"
v-model:current-sort-dir="sortDir"
:data="allRouters.items"
:on-load-more="handleLoadMore"
:end-reached="allRouters.endReached"
:loading="allRouters.loading"
/>
</div>
</div>
</div>
</section>
</page-default>
</template>
<script>
import { defineComponent } from 'vue'
import { mapActions, mapGetters } from 'vuex'
import GetTablePropsMixin from '../../_mixins/GetTableProps'
import PaginationMixin from '../../_mixins/Pagination'
import PageDefault from '../../components/_commons/PageDefault.vue'
import ToolBarTable from '../../components/_commons/ToolBarTable.vue'
import MainTable from '../../components/_commons/MainTable.vue'
export default defineComponent({
name: 'PageTCPRouters',
components: {
PageDefault,
ToolBarTable,
MainTable
},
mixins: [
GetTablePropsMixin,
PaginationMixin({
fetchMethod: 'getAllRoutersWithParams',
scrollerRef: 'mainTable.$refs.scroller',
pollingIntervalTime: 5000
})
],
data () {
return {
filter: '',
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
...mapGetters('tcp', { allRouters: 'allRouters' })
},
watch: {
'status' () {
this.refreshAll()
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeUnmount () {
this.$store.commit('tcp/getAllRoutersClear')
},
methods: {
...mapActions('tcp', { getAllRouters: 'getAllRouters' }),
getAllRoutersWithParams (params) {
return this.getAllRouters({
serviceName: '',
middlewareName: '',
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
...params
})
},
refreshAll () {
if (this.allRouters.loading) {
return
}
this.initFetch()
},
handleLoadMore ({ page = 1 } = {}) {
return this.fetchMore({ page })
}
}
})
</script>
<style scoped lang="scss">
</style>

View file

@ -1,108 +0,0 @@
<template>
<page-default>
<section class="app-section">
<div class="app-section-wrap app-boxed app-boxed-xl q-pl-md q-pr-md q-pt-xl q-pb-xl">
<div class="row no-wrap items-center q-mb-lg">
<tool-bar-table
v-model:status="status"
v-model:filter="filter"
/>
</div>
<div class="row items-center q-col-gutter-lg">
<div class="col-12">
<main-table
ref="mainTable"
v-bind="getTableProps({ type: 'tcp-services' })"
v-model:current-sort="sortBy"
v-model:current-sort-dir="sortDir"
:data="allServices.items"
:on-load-more="handleLoadMore"
:end-reached="allServices.endReached"
:loading="allServices.loading"
/>
</div>
</div>
</div>
</section>
</page-default>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import GetTablePropsMixin from '../../_mixins/GetTableProps'
import PaginationMixin from '../../_mixins/Pagination'
import PageDefault from '../../components/_commons/PageDefault.vue'
import ToolBarTable from '../../components/_commons/ToolBarTable.vue'
import MainTable from '../../components/_commons/MainTable.vue'
export default {
name: 'PageTCPServices',
components: {
PageDefault,
ToolBarTable,
MainTable
},
mixins: [
GetTablePropsMixin,
PaginationMixin({
fetchMethod: 'getAllServicesWithParams',
scrollerRef: 'mainTable.$refs.scroller',
pollingIntervalTime: 5000
})
],
data () {
return {
filter: '',
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
...mapGetters('tcp', { allServices: 'allServices' })
},
watch: {
'status' () {
this.refreshAll()
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeUnmount () {
this.$store.commit('tcp/getAllServicesClear')
},
methods: {
...mapActions('tcp', { getAllServices: 'getAllServices' }),
getAllServicesWithParams (params) {
return this.getAllServices({
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
...params
})
},
refreshAll () {
if (this.allServices.loading) {
return
}
this.initFetch()
},
handleLoadMore ({ page = 1 } = {}) {
return this.fetchMore({ page })
}
}
}
</script>
<style scoped lang="scss">
</style>

View file

@ -0,0 +1,128 @@
import { TcpMiddlewareRender } from './TcpMiddleware'
import { ResourceDetailDataType } from 'hooks/use-resource-detail'
import { renderWithProviders } from 'utils/test'
describe('<TcpMiddlewarePage />', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
<TcpMiddlewareRender name="mock-middleware" data={undefined} error={new Error('Test error')} />,
)
expect(getByTestId('error-text')).toBeInTheDocument()
})
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
<TcpMiddlewareRender name="mock-middleware" data={undefined} error={undefined} />,
)
expect(getByTestId('skeleton')).toBeInTheDocument()
})
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
<TcpMiddlewareRender name="mock-middleware" data={{} as ResourceDetailDataType} error={undefined} />,
)
expect(getByTestId('Not found page')).toBeInTheDocument()
})
it('should render a simple middleware', async () => {
const mockData = {
inFlightConn: {
amount: 10,
},
status: 'enabled',
usedBy: ['router-test-simple@docker'],
name: 'middleware-simple',
provider: 'docker',
type: 'addprefix',
routers: [
{
entryPoints: ['web-redirect'],
middlewares: ['middleware-simple'],
service: 'api2_v2-example-beta1',
rule: 'Host(`server`)',
tls: {},
status: 'enabled',
using: ['web-redirect'],
name: 'router-test-simple@docker',
provider: 'docker',
},
],
}
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<TcpMiddlewareRender name="mock-middleware" data={mockData as any} error={undefined} />,
)
const headings = Array.from(container.getElementsByTagName('h1'))
const titleTags = headings.filter((h1) => h1.innerHTML === 'middleware-simple')
expect(titleTags.length).toBe(1)
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')
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')
})
it('should render a complex middleware', async () => {
const mockData = {
name: 'middleware-complex',
type: 'sample-middleware',
status: 'enabled',
provider: 'the-provider',
usedBy: ['router-test-complex@docker'],
inFlightConn: {
amount: 10,
},
ipWhiteList: {
sourceRange: ['125.0.0.1', '125.0.0.4'],
},
routers: [
{
entryPoints: ['web-redirect'],
middlewares: ['middleware-complex'],
service: 'api2_v2-example-beta1',
rule: 'Host(`server`)',
tls: {},
status: 'enabled',
using: ['web-redirect'],
name: 'router-test-complex@docker',
provider: 'docker',
},
],
}
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<TcpMiddlewareRender name="mock-middleware" data={mockData as any} error={undefined} />,
)
const headings = Array.from(container.getElementsByTagName('h1'))
const titleTags = headings.filter((h1) => h1.innerHTML === 'middleware-complex')
expect(titleTags.length).toBe(1)
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')
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')
})
})

View file

@ -0,0 +1,73 @@
import { Card, Box, H1, Skeleton, styled, Text } from '@traefiklabs/faency'
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 Page from 'layout/Page'
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 (
<Page title={name}>
<Text data-testid="error-text">
Sorry, we could not fetch detail information for this Middleware right now. Please, try again later.
</Text>
</Page>
)
}
if (!data) {
return (
<Page title={name}>
<Skeleton css={{ height: '$7', width: '320px', mb: '$4' }} data-testid="skeleton" />
<MiddlewareGrid>
<DetailSectionSkeleton />
</MiddlewareGrid>
<UsedByRoutersSkeleton />
</Page>
)
}
if (!data.name) {
return <NotFound />
}
return (
<Page title={name}>
<H1 css={{ mb: '$7' }}>{data.name}</H1>
<MiddlewareGrid>
<Card css={{ padding: '$5' }} data-testid="middleware-card">
<RenderMiddleware middleware={data} />
</Card>
</MiddlewareGrid>
<UsedByRoutersSection data-testid="routers-table" data={data} protocol="tcp" />
</Page>
)
}
export const TcpMiddleware = () => {
const { name } = useParams<{ name: string }>()
const { data, error } = useResourceDetail(name!, 'middlewares', 'tcp')
return <TcpMiddlewareRender data={data} error={error} name={name!} />
}
export default TcpMiddleware

View file

@ -0,0 +1,67 @@
import { makeRowRender, TcpMiddlewares as TcpMiddlewaresPage, TcpMiddlewaresRender } from './TcpMiddlewares'
import * as useFetchWithPagination from 'hooks/use-fetch-with-pagination'
import { useFetchWithPaginationMock } from 'utils/mocks'
import { renderWithProviders } from 'utils/test'
describe('<TcpMiddlewaresPage />', () => {
it('should render the middlewares list', () => {
const pages = [
{
inFlightConn: { amount: 10 },
status: 'enabled',
usedBy: ['web@docker'],
name: 'inFlightConn-foo@docker',
provider: 'docker',
type: 'inFlightConn',
},
{
ipWhiteList: { sourceRange: ['125.0.0.1', '125.0.0.4'] },
error: ['message 1', 'message 2'],
status: 'disabled',
usedBy: ['foo@docker', 'bar@file'],
name: 'ipWhiteList@docker',
provider: 'docker',
type: 'ipWhiteList',
},
].map(makeRowRender())
const mock = vi
.spyOn(useFetchWithPagination, 'default')
.mockImplementation(() => useFetchWithPaginationMock({ pages }))
const { container, getByTestId } = renderWithProviders(<TcpMiddlewaresPage />)
expect(mock).toHaveBeenCalled()
expect(getByTestId('TCP Middlewares page')).toBeInTheDocument()
const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1]
expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(2)
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toContain('testid="enabled"')
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toContain('inFlightConn-foo@docker')
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toContain('inFlightConn')
expect(tbody.querySelectorAll('a[role="row"]')[0].querySelector('svg[data-testid="docker"]')).toBeTruthy()
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toContain('testid="disabled"')
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toContain('ipWhiteList@docker')
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toContain('ipWhiteList')
expect(tbody.querySelectorAll('a[role="row"]')[1].querySelector('svg[data-testid="docker"]')).toBeTruthy()
})
it('should render "No data available" when the API returns empty array', async () => {
const { container, getByTestId } = renderWithProviders(
<TcpMiddlewaresRender
error={undefined}
isEmpty={true}
isLoadingMore={false}
isReachingEnd={true}
loadMore={() => {}}
pageCount={1}
pages={[]}
/>,
)
expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]')
const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2]
expect(tfoot.querySelectorAll('div[role="row"]')).toHaveLength(1)
expect(tfoot.querySelectorAll('div[role="row"]')[0].innerHTML).toContain('No data available')
})
})

View file

@ -0,0 +1,125 @@
import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency'
import { useMemo } from 'react'
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 { ResourceStatus } from 'components/resources/ResourceStatus'
import { ScrollTopButton } from 'components/ScrollTopButton'
import { SpinnerLoader } from 'components/SpinnerLoader'
import { searchParamsToState, TableFilter } from 'components/TableFilter'
import SortableTh from 'components/tables/SortableTh'
import Tooltip from 'components/Tooltip'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
import Page from 'layout/Page'
import { parseMiddlewareType } from 'libs/parsers'
export const makeRowRender = (): RenderRowType => {
const TcpMiddlewaresRenderRow = (row) => {
const middlewareType = parseMiddlewareType(row)
return (
<ClickableRow key={row.name} to={`/tcp/middlewares/${row.name}`}>
<AriaTd>
<Tooltip label={row.status}>
<Box css={{ width: '32px', height: '32px' }}>
<ResourceStatus status={row.status} />
</Box>
</Tooltip>
</AriaTd>
<AriaTd>
<TooltipText text={row.name} />
</AriaTd>
<AriaTd>
<TooltipText text={middlewareType} />
</AriaTd>
<AriaTd>
<Tooltip label={row.provider}>
<Box css={{ width: '32px', height: '32px' }}>
<ProviderIcon name={row.provider} />
</Box>
</Tooltip>
</AriaTd>
</ClickableRow>
)
}
return TcpMiddlewaresRenderRow
}
export const TcpMiddlewaresRender = ({
error,
isEmpty,
isLoadingMore,
isReachingEnd,
loadMore,
pageCount,
pages,
}: pagesResponseInterface) => {
const [infiniteRef] = useInfiniteScroll({
loading: isLoadingMore,
hasNextPage: !isReachingEnd && !error,
onLoadMore: loadMore,
})
return (
<>
<AriaTable>
<AriaThead>
<AriaTr>
<SortableTh label="Status" css={{ width: '40px' }} isSortable sortByValue="status" />
<SortableTh label="Name" isSortable sortByValue="name" />
<SortableTh label="Type" isSortable sortByValue="type" />
<SortableTh label="Provider" css={{ width: '75px' }} isSortable sortByValue="provider" />
</AriaTr>
</AriaThead>
<AriaTbody>{pages}</AriaTbody>
{(isEmpty || !!error) && (
<AriaTfoot>
<AriaTr>
<AriaTd fullColSpan>
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
</AriaTd>
</AriaTr>
</AriaTfoot>
)}
</AriaTable>
<Flex css={{ height: 60, alignItems: 'center', justifyContent: 'center' }} ref={infiniteRef}>
{isLoadingMore ? <SpinnerLoader /> : isReachingEnd && pageCount > 1 && <ScrollTopButton />}
</Flex>
</>
)
}
export const TcpMiddlewares = () => {
const renderRow = makeRowRender()
const [searchParams] = useSearchParams()
const query = useMemo(() => searchParamsToState(searchParams), [searchParams])
const { pages, pageCount, isLoadingMore, isReachingEnd, loadMore, error, isEmpty } = useFetchWithPagination(
'/tcp/middlewares',
{
listContextKey: JSON.stringify(query),
renderRow,
renderLoader: () => null,
query,
},
)
return (
<Page title="TCP Middlewares">
<TableFilter />
<TcpMiddlewaresRender
error={error}
isEmpty={isEmpty}
isLoadingMore={isLoadingMore}
isReachingEnd={isReachingEnd}
loadMore={loadMore}
pageCount={pageCount}
pages={pages}
/>
</Page>
)
}

View file

@ -0,0 +1,102 @@
import { TcpRouterRender } from './TcpRouter'
import { ResourceDetailDataType } from 'hooks/use-resource-detail'
import { renderWithProviders } from 'utils/test'
describe('<TcpRouterPage />', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
<TcpRouterRender name="mock-router" data={undefined} error={new Error('Test error')} />,
)
expect(getByTestId('error-text')).toBeInTheDocument()
})
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
<TcpRouterRender name="mock-router" data={undefined} error={undefined} />,
)
expect(getByTestId('skeleton')).toBeInTheDocument()
})
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
<TcpRouterRender name="mock-router" data={{} as ResourceDetailDataType} error={undefined} />,
)
expect(getByTestId('Not found page')).toBeInTheDocument()
})
it('should render the router details', async () => {
const mockData = {
entryPoints: ['web-tcp'],
service: 'tcp-all',
rule: 'HostSNI(`*`)',
status: 'enabled',
using: ['web-secured', 'web'],
name: 'tcp-all@docker',
provider: 'docker',
middlewares: [
{
status: 'enabled',
usedBy: ['foo@docker', 'bar@file'],
name: 'middleware00@docker',
provider: 'docker',
type: 'middleware00',
},
{
status: 'enabled',
usedBy: ['foo@docker', 'bar@file'],
name: 'middleware01@docker',
provider: 'docker',
type: 'middleware01',
},
],
hasValidMiddlewares: true,
entryPointsData: [
{
address: ':8000',
name: 'web',
},
{
address: ':443',
name: 'web-secured',
},
],
}
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<TcpRouterRender name="mock-router" data={mockData as any} error={undefined} />,
)
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</span>')
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(getByTestId('/tcp/services/tcp-all@docker')).toBeInTheDocument()
})
})

View file

@ -0,0 +1,82 @@
import { Flex, styled, Text } from '@traefiklabs/faency'
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 Page from 'layout/Page'
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) => (
<SpacedColumns data-testid="router-details">
<RouterPanel data={data} />
<TlsPanel data={data} />
<MiddlewarePanel data={data} />
</SpacedColumns>
)
type TcpRouterRenderProps = {
data?: ResourceDetailDataType
error?: Error
name: string
}
export const TcpRouterRender = ({ data, error, name }: TcpRouterRenderProps) => {
if (error) {
return (
<Page title={name}>
<Text data-testid="error-text">
Sorry, we could not fetch detail information for this Router right now. Please, try again later.
</Text>
</Page>
)
}
if (!data) {
return (
<Page title={name}>
<Flex css={{ flexDirection: 'row', mb: '70px' }} data-testid="skeleton">
<CardListSection bigDescription />
<CardListSection />
<CardListSection isLast />
</Flex>
<SpacedColumns>
<DetailSectionSkeleton />
<DetailSectionSkeleton />
</SpacedColumns>
</Page>
)
}
if (!data.name) {
return <NotFound />
}
return (
<Page title={name}>
<RouterStructure data={data} protocol="tcp" />
<RouterDetail data={data} />
</Page>
)
}
export const TcpRouter = () => {
const { name } = useParams<{ name: string }>()
const { data, error } = useResourceDetail(name!, 'routers', 'tcp')
return <TcpRouterRender data={data} error={error} name={name!} />
}
export default TcpRouter

View file

@ -0,0 +1,85 @@
import { makeRowRender, TcpRouters as TcpRoutersPage, TcpRoutersRender } from './TcpRouters'
import * as useFetchWithPagination from 'hooks/use-fetch-with-pagination'
import { useFetchWithPaginationMock } from 'utils/mocks'
import { renderWithProviders } from 'utils/test'
describe('<TcpRoutersPage />', () => {
it('should render the routers list', () => {
const pages = [
{
entryPoints: ['web-tcp'],
service: 'tcp-all',
rule: 'HostSNI(`*`)',
status: 'enabled',
using: ['web-secured', 'web'],
name: 'tcp-all@docker00',
provider: 'docker',
},
{
entryPoints: ['web-tcp'],
service: 'tcp-all',
rule: 'HostSNI(`*`)',
status: 'disabled',
using: ['web-secured', 'web'],
name: 'tcp-all@docker01',
provider: 'docker',
},
{
entryPoints: ['web-tcp'],
service: 'tcp-all',
rule: 'HostSNI(`*`)',
status: 'enabled',
using: ['web-secured', 'web'],
name: 'tcp-all@docker02',
provider: 'docker',
},
].map(makeRowRender())
const mock = vi
.spyOn(useFetchWithPagination, 'default')
.mockImplementation(() => useFetchWithPaginationMock({ pages }))
const { container, getByTestId } = renderWithProviders(<TcpRoutersPage />)
expect(mock).toHaveBeenCalled()
expect(getByTestId('TCP Routers page')).toBeInTheDocument()
const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1]
expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(3)
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toContain('testid="enabled"')
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toContain('HostSNI(`*`)')
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toIncludeMultiple(['web-tcp'])
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toContain('tcp-all@docker00')
expect(tbody.querySelectorAll('a[role="row"]')[0].querySelector('svg[data-testid="docker"]')).toBeTruthy()
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toContain('testid="disabled"')
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toContain('HostSNI(`*`)')
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toIncludeMultiple(['web-tcp'])
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toContain('tcp-all@docker01')
expect(tbody.querySelectorAll('a[role="row"]')[1].querySelector('svg[data-testid="docker"]')).toBeTruthy()
expect(tbody.querySelectorAll('a[role="row"]')[2].innerHTML).toContain('testid="enabled"')
expect(tbody.querySelectorAll('a[role="row"]')[2].innerHTML).toContain('HostSNI(`*`)')
expect(tbody.querySelectorAll('a[role="row"]')[2].innerHTML).toIncludeMultiple(['web-tcp'])
expect(tbody.querySelectorAll('a[role="row"]')[2].innerHTML).toContain('tcp-all@docker02')
expect(tbody.querySelectorAll('a[role="row"]')[2].querySelector('svg[data-testid="docker"]')).toBeTruthy()
})
it('should render "No data available" when the API returns empty array', async () => {
const { container, getByTestId } = renderWithProviders(
<TcpRoutersRender
error={undefined}
isEmpty={true}
isLoadingMore={false}
isReachingEnd={true}
loadMore={() => {}}
pageCount={1}
pages={[]}
/>,
)
expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]')
const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2]
expect(tfoot.querySelectorAll('div[role="row"]')).toHaveLength(1)
expect(tfoot.querySelectorAll('div[role="row"]')[0].innerHTML).toContain('No data available')
})
})

View file

@ -0,0 +1,142 @@
import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency'
import { useMemo } from 'react'
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 { ResourceStatus } from 'components/resources/ResourceStatus'
import { ScrollTopButton } from 'components/ScrollTopButton'
import { SpinnerLoader } from 'components/SpinnerLoader'
import { searchParamsToState, TableFilter } from 'components/TableFilter'
import SortableTh from 'components/tables/SortableTh'
import Tooltip from 'components/Tooltip'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
import Page from 'layout/Page'
export const makeRowRender = (): RenderRowType => {
const TcpRoutersRenderRow = (row) => (
<ClickableRow key={row.name} to={`/tcp/routers/${row.name}`}>
<AriaTd>
<Tooltip label={row.status}>
<Box css={{ width: '32px', height: '32px' }}>
<ResourceStatus status={row.status} />
</Box>
</Tooltip>
</AriaTd>
<AriaTd>
{row.tls && (
<Tooltip label="TLS ON">
<Box css={{ width: 24, height: 24 }} data-testid="tls-on">
<FiShield color="#008000" fill="#008000" size={24} />
</Box>
</Tooltip>
)}
</AriaTd>
<AriaTd>
<TooltipText text={row.rule} isTruncated />
</AriaTd>
<AriaTd>{row.entryPoints && row.entryPoints.length > 0 && <Chips items={row.entryPoints} />}</AriaTd>
<AriaTd>
<TooltipText text={row.name} isTruncated />
</AriaTd>
<AriaTd>
<TooltipText text={row.service} isTruncated />
</AriaTd>
<AriaTd>
<Tooltip label={row.provider}>
<Box css={{ width: '32px', height: '32px' }}>
<ProviderIcon name={row.provider} />
</Box>
</Tooltip>
</AriaTd>
<AriaTd>
<TooltipText text={row.priority} isTruncated />
</AriaTd>
</ClickableRow>
)
return TcpRoutersRenderRow
}
export const TcpRoutersRender = ({
error,
isEmpty,
isLoadingMore,
isReachingEnd,
loadMore,
pageCount,
pages,
}: pagesResponseInterface) => {
const [infiniteRef] = useInfiniteScroll({
loading: isLoadingMore,
hasNextPage: !isReachingEnd && !error,
onLoadMore: loadMore,
})
return (
<>
<AriaTable>
<AriaThead>
<AriaTr>
<SortableTh label="Status" css={{ width: '40px' }} isSortable sortByValue="status" />
<SortableTh label="TLS" css={{ width: '40px' }} />
<SortableTh label="Rule" isSortable sortByValue="rule" />
<SortableTh label="Entrypoints" isSortable sortByValue="entryPoints" />
<SortableTh label="Name" isSortable sortByValue="name" />
<SortableTh label="Service" isSortable sortByValue="service" />
<SortableTh label="Provider" isSortable sortByValue="provider" />
<SortableTh label="Priority" isSortable sortByValue="priority" />
</AriaTr>
</AriaThead>
<AriaTbody>{pages}</AriaTbody>
{(isEmpty || !!error) && (
<AriaTfoot>
<AriaTr>
<AriaTd fullColSpan>
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
</AriaTd>
</AriaTr>
</AriaTfoot>
)}
</AriaTable>
<Flex css={{ height: 60, alignItems: 'center', justifyContent: 'center' }} ref={infiniteRef}>
{isLoadingMore ? <SpinnerLoader /> : isReachingEnd && pageCount > 1 && <ScrollTopButton />}
</Flex>
</>
)
}
export const TcpRouters = () => {
const renderRow = makeRowRender()
const [searchParams] = useSearchParams()
const query = useMemo(() => searchParamsToState(searchParams), [searchParams])
const { pages, pageCount, isLoadingMore, isReachingEnd, loadMore, error, isEmpty } = useFetchWithPagination(
'/tcp/routers',
{
listContextKey: JSON.stringify(query),
renderRow,
renderLoader: () => null,
query,
},
)
return (
<Page title="TCP Routers">
<TableFilter />
<TcpRoutersRender
error={error}
isEmpty={isEmpty}
isLoadingMore={isLoadingMore}
isReachingEnd={isReachingEnd}
loadMore={loadMore}
pageCount={pageCount}
pages={pages}
/>
</Page>
)
}

View file

@ -0,0 +1,163 @@
import { TcpServiceRender } from './TcpService'
import { ResourceDetailDataType } from 'hooks/use-resource-detail'
import { renderWithProviders } from 'utils/test'
describe('<TcpServicePage />', () => {
it('should render the error message', () => {
const { getByTestId } = renderWithProviders(
<TcpServiceRender name="mock-service" data={undefined} error={new Error('Test error')} />,
)
expect(getByTestId('error-text')).toBeInTheDocument()
})
it('should render the skeleton', () => {
const { getByTestId } = renderWithProviders(
<TcpServiceRender name="mock-service" data={undefined} error={undefined} />,
)
expect(getByTestId('skeleton')).toBeInTheDocument()
})
it('should render the not found page', () => {
const { getByTestId } = renderWithProviders(
<TcpServiceRender name="mock-service" data={{} as ResourceDetailDataType} error={undefined} />,
)
expect(getByTestId('Not found page')).toBeInTheDocument()
})
it('should render the service', async () => {
const mockData = {
loadBalancer: {
servers: [
{
address: 'http://10.0.1.12:80',
},
],
passHostHeader: true,
terminationDelay: 10,
},
status: 'enabled',
usedBy: ['router-test1@docker'],
name: 'service-test1',
provider: 'docker',
type: 'loadbalancer',
routers: [
{
entryPoints: ['web-redirect'],
middlewares: ['redirect@file'],
service: 'api2_v2-example-beta1',
rule: 'Host(`server`)',
tls: {},
status: 'enabled',
using: ['web-redirect'],
name: 'router-test1@docker',
provider: 'docker',
},
],
}
const { container, getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<TcpServiceRender name="mock-service" data={mockData as any} error={undefined} />,
)
const headings = Array.from(container.getElementsByTagName('h1'))
const titleTags = headings.filter((h1) => h1.innerHTML === 'service-test1')
expect(titleTags.length).toBe(1)
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('Pass Host Header')
expect(serviceDetails.innerHTML).toContain('True')
expect(serviceDetails.innerHTML).toContain('Termination Delay')
expect(serviceDetails.innerHTML).toContain('10 ms')
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')
})
it('should render the service servers from the serverStatus property', async () => {
const mockData = {
loadBalancer: {
terminationDelay: 10,
},
status: 'enabled',
usedBy: ['router-test1@docker', 'router-test2@docker'],
serverStatus: {
'http://10.0.1.12:81': 'UP',
},
name: 'service-test2',
provider: 'docker',
type: 'loadbalancer',
routers: [
{
entryPoints: ['web-redirect'],
middlewares: ['redirect@file'],
service: 'api2_v2-example-beta1',
rule: 'Host(`server`)',
tls: {},
status: 'enabled',
using: ['web-redirect'],
name: 'router-test1@docker',
provider: 'docker',
},
{
entryPoints: ['web-secured'],
service: 'api2_v2-example-beta1',
rule: 'Host(`server`)',
tls: {},
status: 'enabled',
using: ['web-secured'],
name: 'router-test2@docker',
provider: 'docker',
},
],
}
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<TcpServiceRender name="mock-service" data={mockData as any} error={undefined} />,
)
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')
})
it('should not render used by routers table if the usedBy property is empty', async () => {
const mockData = {
status: 'enabled',
usedBy: [],
name: 'service-test3',
provider: 'docker',
type: 'loadbalancer',
routers: [],
}
const { getByTestId } = renderWithProviders(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<TcpServiceRender name="mock-service" data={mockData as any} error={undefined} />,
)
expect(() => {
getByTestId('routers-table')
}).toThrow('Unable to find an element by: [data-testid="routers-table"]')
})
})

View file

@ -0,0 +1,66 @@
import { Flex, H1, Skeleton, styled, Text } from '@traefiklabs/faency'
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 Page from 'layout/Page'
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 TcpServiceRenderProps = {
data?: ResourceDetailDataType
error?: Error
name: string
}
export const TcpServiceRender = ({ data, error, name }: TcpServiceRenderProps) => {
if (error) {
return (
<Page title={name}>
<Text data-testid="error-text">
Sorry, we could not fetch detail information for this Service right now. Please, try again later.
</Text>
</Page>
)
}
if (!data) {
return (
<Page title={name}>
<Skeleton css={{ height: '$7', width: '320px', mb: '$8' }} data-testid="skeleton" />
<SpacedColumns>
<DetailSectionSkeleton narrow />
<DetailSectionSkeleton narrow />
</SpacedColumns>
<UsedByRoutersSkeleton />
</Page>
)
}
if (!data.name) {
return <NotFound />
}
return (
<Page title={name}>
<H1 css={{ mb: '$7' }}>{data.name}</H1>
<ServicePanels data={data} />
<UsedByRoutersSection data={data} protocol="tcp" />
</Page>
)
}
export const TcpService = () => {
const { name } = useParams<{ name: string }>()
const { data, error } = useResourceDetail(name!, 'services', 'tcp')
return <TcpServiceRender data={data} error={error} name={name!} />
}
export default TcpService

View file

@ -0,0 +1,82 @@
import { makeRowRender, TcpServices as TcpServicesPage, TcpServicesRender } from './TcpServices'
import * as useFetchWithPagination from 'hooks/use-fetch-with-pagination'
import { useFetchWithPaginationMock } from 'utils/mocks'
import { renderWithProviders } from 'utils/test'
describe('<TcpServicesPage />', () => {
it('should render the services list', () => {
const pages = [
{
loadBalancer: { terminationDelay: 10, servers: [{ address: '10.0.1.14:8080' }] },
status: 'enabled',
usedBy: ['tcp-all@docker'],
name: 'tcp-all@docker00',
provider: 'docker',
type: 'loadbalancer',
},
{
loadBalancer: { terminationDelay: 10, servers: [{ address: '10.0.1.14:8080' }] },
status: 'disabled',
usedBy: ['tcp-all@docker'],
name: 'tcp-all@docker01',
provider: 'docker',
type: 'loadbalancer',
},
{
loadBalancer: { terminationDelay: 10, servers: [{ address: '10.0.1.14:8080' }] },
status: 'enabled',
usedBy: ['tcp-all@docker'],
name: 'tcp-all@docker02',
provider: 'docker',
type: 'loadbalancer',
},
].map(makeRowRender())
const mock = vi
.spyOn(useFetchWithPagination, 'default')
.mockImplementation(() => useFetchWithPaginationMock({ pages }))
const { container, getByTestId } = renderWithProviders(<TcpServicesPage />)
expect(mock).toHaveBeenCalled()
expect(getByTestId('TCP Services page')).toBeInTheDocument()
const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1]
expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(3)
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toContain('testid="enabled"')
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toContain('tcp-all@docker00')
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toContain('loadbalancer')
expect(tbody.querySelectorAll('a[role="row"]')[0].innerHTML).toContain('1')
expect(tbody.querySelectorAll('a[role="row"]')[0].querySelector('svg[data-testid="docker"]')).toBeTruthy()
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toContain('testid="disabled"')
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toContain('tcp-all@docker01')
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toContain('loadbalancer')
expect(tbody.querySelectorAll('a[role="row"]')[1].innerHTML).toContain('1')
expect(tbody.querySelectorAll('a[role="row"]')[1].querySelector('svg[data-testid="docker"]')).toBeTruthy()
expect(tbody.querySelectorAll('a[role="row"]')[2].innerHTML).toContain('testid="enabled"')
expect(tbody.querySelectorAll('a[role="row"]')[2].innerHTML).toContain('tcp-all@docker02')
expect(tbody.querySelectorAll('a[role="row"]')[2].innerHTML).toContain('loadbalancer')
expect(tbody.querySelectorAll('a[role="row"]')[2].innerHTML).toContain('1')
expect(tbody.querySelectorAll('a[role="row"]')[2].querySelector('svg[data-testid="docker"]')).toBeTruthy()
})
it('should render "No data available" when the API returns empty array', async () => {
const { container, getByTestId } = renderWithProviders(
<TcpServicesRender
error={undefined}
isEmpty={true}
isLoadingMore={false}
isReachingEnd={true}
loadMore={() => {}}
pageCount={1}
pages={[]}
/>,
)
expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]')
const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2]
expect(tfoot.querySelectorAll('div[role="row"]')).toHaveLength(1)
expect(tfoot.querySelectorAll('div[role="row"]')[0].innerHTML).toContain('No data available')
})
})

View file

@ -0,0 +1,124 @@
import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex, Text } from '@traefiklabs/faency'
import { useMemo } from 'react'
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 { ResourceStatus } from 'components/resources/ResourceStatus'
import { ScrollTopButton } from 'components/ScrollTopButton'
import { SpinnerLoader } from 'components/SpinnerLoader'
import { searchParamsToState, TableFilter } from 'components/TableFilter'
import SortableTh from 'components/tables/SortableTh'
import Tooltip from 'components/Tooltip'
import TooltipText from 'components/TooltipText'
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
import Page from 'layout/Page'
export const makeRowRender = (): RenderRowType => {
const TcpServicesRenderRow = (row) => (
<ClickableRow key={row.name} to={`/tcp/services/${row.name}`}>
<AriaTd>
<Tooltip label={row.status}>
<Box css={{ width: '32px', height: '32px' }}>
<ResourceStatus status={row.status} />
</Box>
</Tooltip>
</AriaTd>
<AriaTd>
<TooltipText text={row.name} />
</AriaTd>
<AriaTd>
<TooltipText text={row.type} />
</AriaTd>
<AriaTd>
<Text>{row.loadBalancer?.servers?.length || 0}</Text>
</AriaTd>
<AriaTd>
<Tooltip label={row.provider}>
<Box css={{ width: '32px', height: '32px' }}>
<ProviderIcon name={row.provider} />
</Box>
</Tooltip>
</AriaTd>
</ClickableRow>
)
return TcpServicesRenderRow
}
export const TcpServicesRender = ({
error,
isEmpty,
isLoadingMore,
isReachingEnd,
loadMore,
pageCount,
pages,
}: pagesResponseInterface) => {
const [infiniteRef] = useInfiniteScroll({
loading: isLoadingMore,
hasNextPage: !isReachingEnd && !error,
onLoadMore: loadMore,
})
return (
<>
<AriaTable>
<AriaThead>
<AriaTr>
<SortableTh label="Status" css={{ width: '40px' }} isSortable sortByValue="status" />
<SortableTh label="Name" isSortable sortByValue="name" />
<SortableTh label="Type" isSortable sortByValue="type" />
<SortableTh label="Servers" isSortable sortByValue="servers" />
<SortableTh label="Provider" css={{ width: '75px' }} isSortable sortByValue="provider" />
</AriaTr>
</AriaThead>
<AriaTbody>{pages}</AriaTbody>
{(isEmpty || !!error) && (
<AriaTfoot>
<AriaTr>
<AriaTd fullColSpan>
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
</AriaTd>
</AriaTr>
</AriaTfoot>
)}
</AriaTable>
<Flex css={{ height: 60, alignItems: 'center', justifyContent: 'center' }} ref={infiniteRef}>
{isLoadingMore ? <SpinnerLoader /> : isReachingEnd && pageCount > 1 && <ScrollTopButton />}
</Flex>
</>
)
}
export const TcpServices = () => {
const renderRow = makeRowRender()
const [searchParams] = useSearchParams()
const query = useMemo(() => searchParamsToState(searchParams), [searchParams])
const { pages, pageCount, isLoadingMore, isReachingEnd, loadMore, error, isEmpty } = useFetchWithPagination(
'/tcp/services',
{
listContextKey: JSON.stringify(query),
renderRow,
renderLoader: () => null,
query,
},
)
return (
<Page title="TCP Services">
<TableFilter />
<TcpServicesRender
error={error}
isEmpty={isEmpty}
isLoadingMore={isLoadingMore}
isReachingEnd={isReachingEnd}
loadMore={loadMore}
pageCount={pageCount}
pages={pages}
/>
</Page>
)
}

View file

@ -0,0 +1,6 @@
export { TcpMiddleware } from './TcpMiddleware'
export { TcpMiddlewares } from './TcpMiddlewares'
export { TcpRouter } from './TcpRouter'
export { TcpRouters } from './TcpRouters'
export { TcpService } from './TcpService'
export { TcpServices } from './TcpServices'