Skip to content

Commit e6af23d

Browse files
feat: [M3-7025] - AGLB Details - Certificates Tab (#9576)
* intial build of certs tab * Added changeset: Add AGLB Details - Certificate Tab * fix query key * improve search, make types more strict, show label in browser tab --------- Co-authored-by: Banks Nussman <banks@nussman.us>
1 parent 7858d32 commit e6af23d

File tree

9 files changed

+253
-5
lines changed

9 files changed

+253
-5
lines changed

packages/api-v4/src/aglb/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ export * from './routes';
77
export * from './service-targets';
88

99
export * from './types';
10+
11+
export * from './certificates';

packages/api-v4/src/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ interface FilterConditionTypes {
3535
'+and'?: Filter[];
3636
'+or'?: Filter[] | string[];
3737
'+order_by'?: string;
38-
'+order'?: string;
38+
'+order'?: 'asc' | 'desc';
3939
'+gt'?: number;
4040
'+gte'?: number;
4141
'+lt'?: number;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Add AGLB Details - Certificate Tab ([#9576](https://github.com/linode/manager/pull/9576))

packages/manager/src/components/PaginationFooter/PaginationFooter.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const MIN_PAGE_SIZE = 25;
99

1010
export interface PaginationProps {
1111
count: number;
12-
eventCategory: string;
12+
eventCategory?: string;
1313
fixedSize?: boolean;
1414
page: number;
1515
pageSize: number;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import CloseIcon from '@mui/icons-material/Close';
2+
import { IconButton } from '@mui/material';
3+
import Stack from '@mui/material/Stack';
4+
import React, { useState } from 'react';
5+
import { useParams } from 'react-router-dom';
6+
7+
import { ActionMenu } from 'src/components/ActionMenu';
8+
import { Box } from 'src/components/Box';
9+
import { Button } from 'src/components/Button/Button';
10+
import { CircleProgress } from 'src/components/CircleProgress';
11+
import EnhancedSelect from 'src/components/EnhancedSelect';
12+
import { InputAdornment } from 'src/components/InputAdornment';
13+
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
14+
import { Table } from 'src/components/Table';
15+
import { TableBody } from 'src/components/TableBody';
16+
import { TableCell } from 'src/components/TableCell';
17+
import { TableHead } from 'src/components/TableHead';
18+
import { TableRow } from 'src/components/TableRow';
19+
import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
20+
import { TableRowError } from 'src/components/TableRowError/TableRowError';
21+
import { TableSortCell } from 'src/components/TableSortCell';
22+
import { TextField } from 'src/components/TextField';
23+
import { useOrder } from 'src/hooks/useOrder';
24+
import { usePagination } from 'src/hooks/usePagination';
25+
import { useLoadBalancerCertificatesQuery } from 'src/queries/aglb/certificates';
26+
27+
import type { Certificate, Filter } from '@linode/api-v4';
28+
29+
const PREFERENCE_KEY = 'loadbalancer-certificates';
30+
31+
const CERTIFICATE_TYPE_LABEL_MAP: Record<Certificate['type'], string> = {
32+
ca: 'Service Target Certificate',
33+
downstream: 'TLS Certificate',
34+
};
35+
36+
type CertificateTypeFilter = 'all' | Certificate['type'];
37+
38+
export const LoadBalancerCertificates = () => {
39+
const { loadbalancerId } = useParams<{ loadbalancerId: string }>();
40+
41+
const [type, setType] = useState<CertificateTypeFilter>('all');
42+
const [query, setQuery] = useState<string>();
43+
44+
const pagination = usePagination(1, PREFERENCE_KEY);
45+
46+
const { handleOrderChange, order, orderBy } = useOrder(
47+
{
48+
order: 'desc',
49+
orderBy: 'label',
50+
},
51+
`${PREFERENCE_KEY}-order`
52+
);
53+
54+
const filter: Filter = {
55+
['+order']: order,
56+
['+order_by']: orderBy,
57+
};
58+
59+
// If the user selects a Certificate type filter, API filter by that type.
60+
if (type !== 'all') {
61+
filter['type'] = type;
62+
}
63+
64+
// If the user types in a search query, API filter by the label.
65+
if (query) {
66+
filter['label'] = { '+contains': query };
67+
}
68+
69+
const { data, error, isLoading } = useLoadBalancerCertificatesQuery(
70+
Number(loadbalancerId),
71+
{
72+
page: pagination.page,
73+
page_size: pagination.pageSize,
74+
},
75+
filter
76+
);
77+
78+
if (isLoading) {
79+
return <CircleProgress />;
80+
}
81+
82+
const filterOptions = [
83+
{ label: 'All', value: 'all' },
84+
{ label: 'TLS Certificates', value: 'tls' },
85+
{ label: 'Service Target Certificates', value: 'ca' },
86+
];
87+
88+
return (
89+
<>
90+
<Stack
91+
alignItems="flex-end"
92+
direction="row"
93+
flexWrap="wrap"
94+
gap={2}
95+
mb={2}
96+
mt={1.5}
97+
>
98+
<EnhancedSelect
99+
styles={{
100+
container: () => ({
101+
maxWidth: '200px',
102+
}),
103+
}}
104+
isClearable={false}
105+
label="Certificate Type"
106+
noMarginTop
107+
onChange={(option) => setType(option?.value as CertificateTypeFilter)}
108+
options={filterOptions}
109+
value={filterOptions.find((option) => option.value === type) ?? null}
110+
/>
111+
<TextField
112+
InputProps={{
113+
endAdornment: (
114+
<InputAdornment position="end">
115+
<IconButton
116+
aria-label="Clear"
117+
onClick={() => setQuery('')}
118+
size="small"
119+
sx={{ padding: 'unset' }}
120+
>
121+
<CloseIcon
122+
color="inherit"
123+
sx={{ color: '#aaa !important' }}
124+
/>
125+
</IconButton>
126+
</InputAdornment>
127+
),
128+
}}
129+
hideLabel
130+
label="Filter"
131+
onChange={(e) => setQuery(e.target.value)}
132+
placeholder="Filter"
133+
style={{ minWidth: '320px' }}
134+
value={query}
135+
/>
136+
<Box flexGrow={1} />
137+
<Button buttonType="primary">Upload Certificate</Button>
138+
</Stack>
139+
<Table>
140+
<TableHead>
141+
<TableRow>
142+
<TableSortCell
143+
active={orderBy === 'label'}
144+
direction={order}
145+
handleClick={handleOrderChange}
146+
label="label"
147+
>
148+
Label
149+
</TableSortCell>
150+
<TableSortCell
151+
active={orderBy === 'type'}
152+
direction={order}
153+
handleClick={handleOrderChange}
154+
label="type"
155+
>
156+
Type
157+
</TableSortCell>
158+
<TableCell></TableCell>
159+
</TableRow>
160+
</TableHead>
161+
<TableBody>
162+
{error && <TableRowError colSpan={3} message={error?.[0].reason} />}
163+
{data?.results === 0 && <TableRowEmpty colSpan={3} />}
164+
{data?.data.map(({ label, type }) => (
165+
<TableRow key={`${label}-${type}`}>
166+
<TableCell>{label}</TableCell>
167+
<TableCell>{CERTIFICATE_TYPE_LABEL_MAP[type]}</TableCell>
168+
<TableCell actionCell>
169+
<ActionMenu
170+
actionsList={[
171+
{ onClick: () => null, title: 'Edit' },
172+
{ onClick: () => null, title: 'Delete' },
173+
]}
174+
ariaLabel={`Action Menu for certificate ${label}`}
175+
/>
176+
</TableCell>
177+
</TableRow>
178+
))}
179+
</TableBody>
180+
</Table>
181+
<PaginationFooter
182+
count={data?.results ?? 0}
183+
handlePageChange={pagination.handlePageChange}
184+
handleSizeChange={pagination.handlePageSizeChange}
185+
page={pagination.page}
186+
pageSize={pagination.pageSize}
187+
/>
188+
</>
189+
);
190+
};

packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerDetail.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ const LoadBalancerSummary = React.lazy(() =>
1616
}))
1717
);
1818

19+
const LoadBalancerCertificates = React.lazy(() =>
20+
import('./LoadBalancerCertificates').then((module) => ({
21+
default: module.LoadBalancerCertificates,
22+
}))
23+
);
24+
1925
const LoadBalancerDetailLanding = () => {
2026
const history = useHistory();
2127

@@ -59,7 +65,7 @@ const LoadBalancerDetailLanding = () => {
5965

6066
return (
6167
<>
62-
<DocumentTitleSegment segment={loadbalancerId} />
68+
<DocumentTitleSegment segment={loadbalancer?.label ?? ''} />
6369
<LandingHeader
6470
breadcrumbProps={{
6571
crumbOverrides: [
@@ -88,7 +94,11 @@ const LoadBalancerDetailLanding = () => {
8894
<SafeTabPanel index={1}>1</SafeTabPanel>
8995
<SafeTabPanel index={2}>2</SafeTabPanel>
9096
<SafeTabPanel index={3}>3</SafeTabPanel>
91-
<SafeTabPanel index={4}>4</SafeTabPanel>
97+
<SafeTabPanel index={4}>
98+
<React.Suspense fallback={<SuspenseLoader />}>
99+
<LoadBalancerCertificates />
100+
</React.Suspense>
101+
</SafeTabPanel>
92102
<SafeTabPanel index={5}>5</SafeTabPanel>
93103
</TabPanels>
94104
</Tabs>

packages/manager/src/features/Support/SupportTickets/ticketUtils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const getTicketsPage = (
4040
ticketStatus: 'all' | 'closed' | 'open'
4141
) => {
4242
const status = getStatusFilter(ticketStatus);
43-
const ordering = { '+order': 'desc', '+order_by': 'opened' };
43+
const ordering = { '+order': 'desc', '+order_by': 'opened' } as const;
4444
const filter = { ...status, ...ordering, ...filters };
4545
return getTickets(params, filter);
4646
};

packages/manager/src/mocks/serverHandlers.ts

+17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
accountTransferFactory,
1919
appTokenFactory,
2020
betaFactory,
21+
certificateFactory,
2122
configurationFactory,
2223
contactFactory,
2324
createRouteFactory,
@@ -389,6 +390,22 @@ const aglb = [
389390
return res(ctx.json({}));
390391
}
391392
),
393+
// Certificates
394+
rest.get('*/v4beta/aglb/:id/certificates', (req, res, ctx) => {
395+
const certificates = certificateFactory.buildList(3);
396+
return res(ctx.json(makeResourcePage(certificates)));
397+
}),
398+
rest.post('*/v4beta/aglb/:id/certificates', (req, res, ctx) => {
399+
return res(ctx.json(certificateFactory.build()));
400+
}),
401+
rest.put('*/v4beta/aglb/:id/certificates/:certId', (req, res, ctx) => {
402+
const id = Number(req.params.certId);
403+
const body = req.body as any;
404+
return res(ctx.json(certificateFactory.build({ id, ...body })));
405+
}),
406+
rest.delete('*/v4beta/aglb/:id/certificates/:certId', (req, res, ctx) => {
407+
return res(ctx.json({}));
408+
}),
392409
];
393410

394411
const vpc = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { getLoadbalancerCertificates } from '@linode/api-v4';
2+
import { useQuery } from 'react-query';
3+
4+
import { QUERY_KEY } from './loadbalancers';
5+
6+
import type {
7+
APIError,
8+
Certificate,
9+
Filter,
10+
Params,
11+
ResourcePage,
12+
} from '@linode/api-v4';
13+
14+
export const useLoadBalancerCertificatesQuery = (
15+
id: number,
16+
params: Params,
17+
filter: Filter
18+
) => {
19+
return useQuery<ResourcePage<Certificate>, APIError[]>(
20+
[QUERY_KEY, 'loadbalancer', id, 'certificates', params, filter],
21+
() => getLoadbalancerCertificates(id, params, filter),
22+
{ keepPreviousData: true }
23+
);
24+
};

0 commit comments

Comments
 (0)