Skip to content

Commit 249a860

Browse files
upcoming: [DPS-34038] - Add destinations list (linode#12627)
* upcoming: [DPS-34038] - Add destinations list * Update packages/manager/.changeset/pr-12627-upcoming-features-1754307511439.md Co-authored-by: Dajahi Wiley <[email protected]> --------- Co-authored-by: Dajahi Wiley <[email protected]>
1 parent cfc2aa9 commit 249a860

File tree

21 files changed

+419
-45
lines changed

21 files changed

+419
-45
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/api-v4": Upcoming Features
3+
---
4+
5+
API endpoint for Datastream - Create Destination ([#12627](https://github.com/linode/manager/pull/12627))

packages/api-v4/src/account/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export const EventActionKeys = [
343343
'database_update',
344344
'database_migrate',
345345
'database_upgrade',
346+
'destination_create',
346347
'disk_create',
347348
'disk_delete',
348349
'disk_duplicate',
@@ -468,6 +469,7 @@ export const EventActionKeys = [
468469
'stackscript_publicize',
469470
'stackscript_revise',
470471
'stackscript_update',
472+
'stream_create',
471473
'subnet_create',
472474
'subnet_delete',
473475
'subnet_update',
@@ -489,7 +491,6 @@ export const EventActionKeys = [
489491
'user_ssh_key_delete',
490492
'user_ssh_key_update',
491493
'user_update',
492-
'stream_create',
493494
'volume_attach',
494495
'volume_clone',
495496
'volume_create',

packages/api-v4/src/datastream/destinations.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import { createDestinationSchema } from '@linode/validation';
2+
13
import { BETA_API_ROOT } from '../constants';
2-
import Request, { setMethod, setParams, setURL, setXFilter } from '../request';
4+
import Request, {
5+
setData,
6+
setMethod,
7+
setParams,
8+
setURL,
9+
setXFilter,
10+
} from '../request';
311

412
import type { Filter, ResourcePage as Page, Params } from '../types';
5-
import type { Destination } from './types';
13+
import type { CreateDestinationPayload, Destination } from './types';
614

715
/**
816
* Returns all the information about a specified Destination.
@@ -29,3 +37,15 @@ export const getDestinations = (params?: Params, filter?: Filter) =>
2937
setParams(params),
3038
setXFilter(filter),
3139
);
40+
41+
/**
42+
* Adds a new Destination.
43+
*
44+
* @param data { object } Data for type, label, etc.
45+
*/
46+
export const createDestination = (data: CreateDestinationPayload) =>
47+
Request<Destination>(
48+
setData(data, createDestinationSchema),
49+
setURL(`${BETA_API_ROOT}/monitor/streams/destinations`),
50+
setMethod('POST'),
51+
);

packages/api-v4/src/datastream/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,9 @@ export interface CreateStreamPayload {
108108
status?: StreamStatus;
109109
type: StreamType;
110110
}
111+
112+
export interface CreateDestinationPayload {
113+
details: CustomHTTPsDetails | LinodeObjectStorageDetails;
114+
label: string;
115+
type: DestinationType;
116+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Add Destinations list for DataStream page and POST mock handler for Destination Create ([#12627](https://github.com/linode/manager/pull/12627))

packages/manager/src/features/DataStream/Destinations/DestinationCreate/DestinationCreate.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { yupResolver } from '@hookform/resolvers/yup';
22
import { destinationType } from '@linode/api-v4';
3+
import { useCreateDestinationMutation } from '@linode/queries';
34
import { Autocomplete, Box, Button, Paper, TextField } from '@linode/ui';
45
import { createDestinationSchema } from '@linode/validation';
56
import { useTheme } from '@mui/material/styles';
7+
import { useNavigate } from '@tanstack/react-router';
68
import * as React from 'react';
79
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
810

@@ -17,6 +19,8 @@ import type { CreateDestinationForm } from 'src/features/DataStream/Shared/types
1719

1820
export const DestinationCreate = () => {
1921
const theme = useTheme();
22+
const { mutateAsync: createDestination } = useCreateDestinationMutation();
23+
const navigate = useNavigate();
2024

2125
const landingHeaderProps: LandingHeaderProps = {
2226
breadcrumbProps: {
@@ -50,15 +54,20 @@ export const DestinationCreate = () => {
5054
name: 'type',
5155
});
5256

53-
const onSubmit = handleSubmit(async () => {});
57+
const onSubmit = () => {
58+
const payload = form.getValues();
59+
createDestination(payload).then(() => {
60+
navigate({ to: '/datastream/destinations' });
61+
});
62+
};
5463

5564
return (
5665
<>
5766
<DocumentTitleSegment segment="Create Destination" />
5867
<LandingHeader {...landingHeaderProps} />
5968
<Paper>
6069
<FormProvider {...form}>
61-
<form id="createDestinationForm" onSubmit={onSubmit}>
70+
<form id="createDestinationForm" onSubmit={handleSubmit(onSubmit)}>
6271
<Controller
6372
control={control}
6473
name="type"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Hidden } from '@linode/ui';
2+
import * as React from 'react';
3+
4+
import { DateTimeDisplay } from 'src/components/DateTimeDisplay';
5+
import { TableCell } from 'src/components/TableCell';
6+
import { TableRow } from 'src/components/TableRow';
7+
import { getDestinationTypeOption } from 'src/features/DataStream/dataStreamUtils';
8+
9+
import type { Destination } from '@linode/api-v4';
10+
11+
interface DestinationTableRowProps {
12+
destination: Destination;
13+
}
14+
15+
export const DestinationTableRow = React.memo(
16+
(props: DestinationTableRowProps) => {
17+
const { destination } = props;
18+
19+
return (
20+
<TableRow key={destination.id}>
21+
<TableCell>{destination.label}</TableCell>
22+
<TableCell>
23+
{getDestinationTypeOption(destination.type)?.label}
24+
</TableCell>
25+
<TableCell>{destination.id}</TableCell>
26+
<Hidden smDown>
27+
<TableCell>
28+
<DateTimeDisplay value={destination.created} />
29+
</TableCell>
30+
</Hidden>
31+
<TableCell>
32+
<DateTimeDisplay value={destination.updated} />
33+
</TableCell>
34+
</TableRow>
35+
);
36+
}
37+
);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { waitForElementToBeRemoved } from '@testing-library/react';
2+
import * as React from 'react';
3+
4+
import { destinationFactory } from 'src/factories/datastream';
5+
import { DestinationsLanding } from 'src/features/DataStream/Destinations/DestinationsLanding';
6+
import { makeResourcePage } from 'src/mocks/serverHandlers';
7+
import { http, HttpResponse, server } from 'src/mocks/testServer';
8+
import { renderWithTheme } from 'src/utilities/testHelpers';
9+
10+
const loadingTestId = 'circle-progress';
11+
12+
describe('Destinations Landing Table', () => {
13+
it('should render destinations landing table with items PaginationFooter', async () => {
14+
server.use(
15+
http.get('*/monitor/streams/destinations', () => {
16+
return HttpResponse.json(
17+
makeResourcePage(destinationFactory.buildList(30))
18+
);
19+
})
20+
);
21+
22+
const { getByText, queryByTestId, getByTestId } = renderWithTheme(
23+
<DestinationsLanding />
24+
);
25+
26+
const loadingElement = queryByTestId(loadingTestId);
27+
if (loadingElement) {
28+
await waitForElementToBeRemoved(loadingElement);
29+
}
30+
31+
// Table column headers
32+
getByText('Name');
33+
getByText('Type');
34+
getByText('ID');
35+
getByText('Last Modified');
36+
37+
// PaginationFooter
38+
const paginationFooterSelectPageSizeInput = getByTestId(
39+
'textfield-input'
40+
) as HTMLInputElement;
41+
expect(paginationFooterSelectPageSizeInput.value).toBe('Show 25');
42+
});
43+
44+
it('should render images landing empty state', async () => {
45+
server.use(
46+
http.get('*/monitor/streams/destinations', () => {
47+
return HttpResponse.json(makeResourcePage([]));
48+
})
49+
);
50+
51+
const { getByText, queryByTestId } = renderWithTheme(
52+
<DestinationsLanding />
53+
);
54+
55+
const loadingElement = queryByTestId(loadingTestId);
56+
if (loadingElement) {
57+
await waitForElementToBeRemoved(loadingElement);
58+
}
59+
60+
getByText((text) => text.includes('Create a destination for cloud logs'));
61+
});
62+
});
Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,150 @@
1+
import { useDestinationsQuery } from '@linode/queries';
2+
import { CircleProgress, ErrorState, Hidden } from '@linode/ui';
3+
import { TableBody, TableHead, TableRow } from '@mui/material';
4+
import Table from '@mui/material/Table';
5+
import { useNavigate } from '@tanstack/react-router';
16
import * as React from 'react';
27

8+
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
9+
import { TableSortCell } from 'src/components/TableSortCell';
10+
import {
11+
DESTINATIONS_TABLE_DEFAULT_ORDER,
12+
DESTINATIONS_TABLE_DEFAULT_ORDER_BY,
13+
DESTINATIONS_TABLE_PREFERENCE_KEY,
14+
} from 'src/features/DataStream/Destinations/constants';
315
import { DestinationsLandingEmptyState } from 'src/features/DataStream/Destinations/DestinationsLandingEmptyState';
16+
import { DestinationTableRow } from 'src/features/DataStream/Destinations/DestinationTableRow';
17+
import { DataStreamTabHeader } from 'src/features/DataStream/Shared/DataStreamTabHeader/DataStreamTabHeader';
18+
import { useOrderV2 } from 'src/hooks/useOrderV2';
19+
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
420

521
export const DestinationsLanding = () => {
6-
return <DestinationsLandingEmptyState />;
22+
const navigate = useNavigate();
23+
24+
const pagination = usePaginationV2({
25+
currentRoute: '/datastream/destinations',
26+
preferenceKey: DESTINATIONS_TABLE_PREFERENCE_KEY,
27+
});
28+
29+
const { handleOrderChange, order, orderBy } = useOrderV2({
30+
initialRoute: {
31+
defaultOrder: {
32+
order: DESTINATIONS_TABLE_DEFAULT_ORDER,
33+
orderBy: DESTINATIONS_TABLE_DEFAULT_ORDER_BY,
34+
},
35+
from: '/datastream/destinations',
36+
},
37+
preferenceKey: `destinations-order`,
38+
});
39+
40+
const filter = {
41+
['+order']: order,
42+
['+order_by']: orderBy,
43+
};
44+
45+
const {
46+
data: destinations,
47+
isLoading,
48+
error,
49+
} = useDestinationsQuery(
50+
{
51+
page: pagination.page,
52+
page_size: pagination.pageSize,
53+
},
54+
filter
55+
);
56+
57+
const navigateToCreate = () => {
58+
navigate({ to: '/datastream/destinations/create' });
59+
};
60+
61+
if (isLoading) {
62+
return <CircleProgress />;
63+
}
64+
65+
if (error) {
66+
return (
67+
<ErrorState errorText="There was an error retrieving your destinations. Please reload and try again." />
68+
);
69+
}
70+
71+
if (!destinations?.data.length) {
72+
return (
73+
<DestinationsLandingEmptyState navigateToCreate={navigateToCreate} />
74+
);
75+
}
76+
77+
return (
78+
<>
79+
<DataStreamTabHeader
80+
entity="Destination"
81+
loading={isLoading}
82+
onButtonClick={navigateToCreate}
83+
/>
84+
<Table>
85+
<TableHead>
86+
<TableRow>
87+
<TableSortCell
88+
active={orderBy === 'label'}
89+
direction={order}
90+
handleClick={handleOrderChange}
91+
label="label"
92+
>
93+
Name
94+
</TableSortCell>
95+
<TableSortCell
96+
active={orderBy === 'type'}
97+
direction={order}
98+
handleClick={handleOrderChange}
99+
label="type"
100+
>
101+
Type
102+
</TableSortCell>
103+
<TableSortCell
104+
active={orderBy === 'id'}
105+
direction={order}
106+
handleClick={handleOrderChange}
107+
label="id"
108+
>
109+
ID
110+
</TableSortCell>
111+
<Hidden smDown>
112+
<TableSortCell
113+
active={orderBy === 'created'}
114+
direction={order}
115+
handleClick={handleOrderChange}
116+
label="created"
117+
>
118+
Creation Time
119+
</TableSortCell>
120+
</Hidden>
121+
<TableSortCell
122+
active={orderBy === 'updated'}
123+
direction={order}
124+
handleClick={handleOrderChange}
125+
label="updated"
126+
>
127+
Last Modified
128+
</TableSortCell>
129+
</TableRow>
130+
</TableHead>
131+
<TableBody>
132+
{destinations?.data.map((destination) => (
133+
<DestinationTableRow
134+
destination={destination}
135+
key={destination.id}
136+
/>
137+
))}
138+
</TableBody>
139+
</Table>
140+
<PaginationFooter
141+
count={destinations?.results || 0}
142+
eventCategory="Destinations Table"
143+
handlePageChange={pagination.handlePageChange}
144+
handleSizeChange={pagination.handlePageSizeChange}
145+
page={pagination.page}
146+
pageSize={pagination.pageSize}
147+
/>
148+
</>
149+
);
7150
};

0 commit comments

Comments
 (0)