Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [M3-7025] - AGLB Details - Certificates Tab #9576

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/api-v4/src/aglb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export * from './routes';
export * from './service-targets';

export * from './types';

export * from './certificates';
2 changes: 1 addition & 1 deletion packages/api-v4/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface FilterConditionTypes {
'+and'?: Filter[];
'+or'?: Filter[] | string[];
'+order_by'?: string;
'+order'?: string;
'+order'?: 'asc' | 'desc';
'+gt'?: number;
'+gte'?: number;
'+lt'?: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add AGLB Details - Certificate Tab ([#9576](https://github.com/linode/manager/pull/9576))
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const MIN_PAGE_SIZE = 25;

export interface PaginationProps {
count: number;
eventCategory: string;
eventCategory?: string;
fixedSize?: boolean;
page: number;
pageSize: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import CloseIcon from '@mui/icons-material/Close';
import { IconButton } from '@mui/material';
import Stack from '@mui/material/Stack';
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';

import { ActionMenu } from 'src/components/ActionMenu';
import { Box } from 'src/components/Box';
import { Button } from 'src/components/Button/Button';
import { CircleProgress } from 'src/components/CircleProgress';
import EnhancedSelect from 'src/components/EnhancedSelect';
import { InputAdornment } from 'src/components/InputAdornment';
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
import { Table } from 'src/components/Table';
import { TableBody } from 'src/components/TableBody';
import { TableCell } from 'src/components/TableCell';
import { TableHead } from 'src/components/TableHead';
import { TableRow } from 'src/components/TableRow';
import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
import { TableRowError } from 'src/components/TableRowError/TableRowError';
import { TableSortCell } from 'src/components/TableSortCell';
import { TextField } from 'src/components/TextField';
import { useOrder } from 'src/hooks/useOrder';
import { usePagination } from 'src/hooks/usePagination';
import { useLoadBalancerCertificatesQuery } from 'src/queries/aglb/certificates';

import type { Certificate, Filter } from '@linode/api-v4';

const PREFERENCE_KEY = 'loadbalancer-certificates';

const CERTIFICATE_TYPE_LABEL_MAP: Record<Certificate['type'], string> = {
ca: 'Service Target Certificate',
downstream: 'TLS Certificate',
};

type CertificateTypeFilter = 'all' | Certificate['type'];

export const LoadBalancerCertificates = () => {
const { loadbalancerId } = useParams<{ loadbalancerId: string }>();

const [type, setType] = useState<CertificateTypeFilter>('all');
const [query, setQuery] = useState<string>();

const pagination = usePagination(1, PREFERENCE_KEY);

const { handleOrderChange, order, orderBy } = useOrder(
{
order: 'desc',
orderBy: 'label',
},
`${PREFERENCE_KEY}-order`
);

const filter: Filter = {
['+order']: order,
['+order_by']: orderBy,
};

// If the user selects a Certificate type filter, API filter by that type.
if (type !== 'all') {
filter['type'] = type;
}

// If the user types in a search query, API filter by the label.
if (query) {
filter['label'] = { '+contains': query };
}

const { data, error, isLoading } = useLoadBalancerCertificatesQuery(
Number(loadbalancerId),
{
page: pagination.page,
page_size: pagination.pageSize,
},
filter
);

if (isLoading) {
return <CircleProgress />;
}

const filterOptions = [
{ label: 'All', value: 'all' },
{ label: 'TLS Certificates', value: 'tls' },
{ label: 'Service Target Certificates', value: 'ca' },
];

return (
<>
<Stack
alignItems="flex-end"
direction="row"
flexWrap="wrap"
gap={2}
mb={2}
mt={1.5}
>
<EnhancedSelect
styles={{
container: () => ({
maxWidth: '200px',
}),
}}
isClearable={false}
label="Certificate Type"
noMarginTop
onChange={(option) => setType(option?.value as CertificateTypeFilter)}
options={filterOptions}
value={filterOptions.find((option) => option.value === type) ?? null}
/>
<TextField
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Clear"
onClick={() => setQuery('')}
size="small"
sx={{ padding: 'unset' }}
>
<CloseIcon
color="inherit"
sx={{ color: '#aaa !important' }}
/>
</IconButton>
</InputAdornment>
),
}}
hideLabel
label="Filter"
onChange={(e) => setQuery(e.target.value)}
placeholder="Filter"
style={{ minWidth: '320px' }}
value={query}
/>
<Box flexGrow={1} />
<Button buttonType="primary">Upload Certificate</Button>
</Stack>
<Table>
<TableHead>
<TableRow>
<TableSortCell
active={orderBy === 'label'}
direction={order}
handleClick={handleOrderChange}
label="label"
>
Label
</TableSortCell>
<TableSortCell
active={orderBy === 'type'}
direction={order}
handleClick={handleOrderChange}
label="type"
>
Type
</TableSortCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{error && <TableRowError colSpan={3} message={error?.[0].reason} />}
{data?.results === 0 && <TableRowEmpty colSpan={3} />}
{data?.data.map(({ label, type }) => (
<TableRow key={`${label}-${type}`}>
<TableCell>{label}</TableCell>
<TableCell>{CERTIFICATE_TYPE_LABEL_MAP[type]}</TableCell>
<TableCell actionCell>
<ActionMenu
actionsList={[
{ onClick: () => null, title: 'Edit' },
{ onClick: () => null, title: 'Delete' },
]}
ariaLabel={`Action Menu for certificate ${label}`}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<PaginationFooter
count={data?.results ?? 0}
handlePageChange={pagination.handlePageChange}
handleSizeChange={pagination.handlePageSizeChange}
page={pagination.page}
pageSize={pagination.pageSize}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ const LoadBalancerSummary = React.lazy(() =>
}))
);

const LoadBalancerCertificates = React.lazy(() =>
import('./LoadBalancerCertificates').then((module) => ({
default: module.LoadBalancerCertificates,
}))
);

const LoadBalancerDetailLanding = () => {
const history = useHistory();

Expand Down Expand Up @@ -59,7 +65,7 @@ const LoadBalancerDetailLanding = () => {

return (
<>
<DocumentTitleSegment segment={loadbalancerId} />
<DocumentTitleSegment segment={loadbalancer?.label ?? ''} />
<LandingHeader
breadcrumbProps={{
crumbOverrides: [
Expand Down Expand Up @@ -88,7 +94,11 @@ const LoadBalancerDetailLanding = () => {
<SafeTabPanel index={1}>1</SafeTabPanel>
<SafeTabPanel index={2}>2</SafeTabPanel>
<SafeTabPanel index={3}>3</SafeTabPanel>
<SafeTabPanel index={4}>4</SafeTabPanel>
<SafeTabPanel index={4}>
<React.Suspense fallback={<SuspenseLoader />}>
<LoadBalancerCertificates />
</React.Suspense>
</SafeTabPanel>
<SafeTabPanel index={5}>5</SafeTabPanel>
</TabPanels>
</Tabs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const getTicketsPage = (
ticketStatus: 'all' | 'closed' | 'open'
) => {
const status = getStatusFilter(ticketStatus);
const ordering = { '+order': 'desc', '+order_by': 'opened' };
const ordering = { '+order': 'desc', '+order_by': 'opened' } as const;
const filter = { ...status, ...ordering, ...filters };
return getTickets(params, filter);
};
17 changes: 17 additions & 0 deletions packages/manager/src/mocks/serverHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
accountTransferFactory,
appTokenFactory,
betaFactory,
certificateFactory,
configurationFactory,
contactFactory,
createRouteFactory,
Expand Down Expand Up @@ -389,6 +390,22 @@ const aglb = [
return res(ctx.json({}));
}
),
// Certificates
rest.get('*/v4beta/aglb/:id/certificates', (req, res, ctx) => {
const certificates = certificateFactory.buildList(3);
return res(ctx.json(makeResourcePage(certificates)));
}),
rest.post('*/v4beta/aglb/:id/certificates', (req, res, ctx) => {
return res(ctx.json(certificateFactory.build()));
}),
rest.put('*/v4beta/aglb/:id/certificates/:certId', (req, res, ctx) => {
const id = Number(req.params.certId);
const body = req.body as any;
return res(ctx.json(certificateFactory.build({ id, ...body })));
}),
rest.delete('*/v4beta/aglb/:id/certificates/:certId', (req, res, ctx) => {
return res(ctx.json({}));
}),
];

const vpc = [
Expand Down
24 changes: 24 additions & 0 deletions packages/manager/src/queries/aglb/certificates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getLoadbalancerCertificates } from '@linode/api-v4';
import { useQuery } from 'react-query';

import { QUERY_KEY } from './loadbalancers';

import type {
APIError,
Certificate,
Filter,
Params,
ResourcePage,
} from '@linode/api-v4';

export const useLoadBalancerCertificatesQuery = (
id: number,
params: Params,
filter: Filter
) => {
return useQuery<ResourcePage<Certificate>, APIError[]>(
[QUERY_KEY, 'loadbalancer', id, 'certificates', params, filter],
() => getLoadbalancerCertificates(id, params, filter),
{ keepPreviousData: true }
);
};