Skip to content

Commit 9869d96

Browse files
committed
upcoming: [DPS-35924] Custom HTTPs form - first part
1 parent 64174db commit 9869d96

File tree

10 files changed

+840
-198
lines changed

10 files changed

+840
-198
lines changed

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,19 @@ export interface CustomHTTPsDetails {
8484
}
8585

8686
interface ClientCertificateDetails {
87-
client_ca_certificate: string;
88-
client_certificate: string;
89-
client_private_key: string;
90-
tls_hostname: string;
87+
client_ca_certificate?: string;
88+
client_certificate?: string;
89+
client_private_key?: string;
90+
tls_hostname?: string;
9191
}
9292

93-
type AuthenticationType = 'basic' | 'none';
93+
export const authenticationType = {
94+
Basic: 'basic',
95+
None: 'none',
96+
} as const;
97+
98+
export type AuthenticationType =
99+
(typeof authenticationType)[keyof typeof authenticationType];
94100

95101
interface Authentication {
96102
details?: AuthenticationDetails;

packages/manager/src/featureFlags.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ interface AclpLogsFlag extends BetaFeatureFlag {
120120
* This property indicates whether to bypass account capabilities check or not
121121
*/
122122
bypassAccountCapabilities?: boolean;
123+
/**
124+
* This property indicates whether to show Custom HTTPs destination type
125+
*/
126+
customHttpsEnabled?: boolean;
123127
}
124128

125129
interface LkeEnterpriseFlag extends BaseFeatureFlag {

packages/manager/src/features/Delivery/Destinations/DestinationForm/DestinationCreate.test.tsx

Lines changed: 161 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';
1212
import { DestinationCreate } from './DestinationCreate';
1313

1414
import type { CreateDestinationPayload } from '@linode/api-v4';
15+
import type { Flags } from 'src/featureFlags';
1516

1617
describe('DestinationCreate', () => {
1718
const renderDestinationCreate = (
19+
flags: Partial<Flags>,
1820
defaultValues?: Partial<CreateDestinationPayload>
1921
) => {
2022
renderWithThemeAndHookFormContext({
@@ -25,88 +27,183 @@ describe('DestinationCreate', () => {
2527
...defaultValues,
2628
},
2729
},
30+
options: {
31+
flags,
32+
},
2833
});
2934
};
3035

31-
it('should render disabled Destination Type input with proper selection', async () => {
32-
renderDestinationCreate();
36+
describe('when customHttpsEnabled feature flag is set to false', () => {
37+
const flags = {
38+
aclpLogs: {
39+
enabled: true,
40+
beta: false,
41+
customHttpsEnabled: false,
42+
},
43+
};
3344

34-
const destinationTypeAutocomplete =
35-
screen.getByLabelText('Destination Type');
45+
it('should render disabled Destination Type input with proper selection', async () => {
46+
renderDestinationCreate(flags);
3647

37-
expect(destinationTypeAutocomplete).toBeDisabled();
38-
expect(destinationTypeAutocomplete).toHaveValue('Akamai Object Storage');
39-
});
48+
const destinationTypeAutocomplete =
49+
screen.getByLabelText('Destination Type');
4050

41-
it(
42-
'should render all inputs for Akamai Object Storage type and allow to fill out them',
43-
{ timeout: 10000 },
44-
async () => {
45-
renderDestinationCreate({ label: '' });
51+
expect(destinationTypeAutocomplete).toBeDisabled();
52+
expect(destinationTypeAutocomplete).toHaveValue('Akamai Object Storage');
53+
});
4654

47-
const destinationNameInput = screen.getByLabelText('Destination Name');
48-
await userEvent.type(destinationNameInput, 'Test');
49-
const hostInput = screen.getByLabelText('Host');
50-
await userEvent.type(hostInput, 'test');
51-
const bucketInput = screen.getByLabelText('Bucket');
52-
await userEvent.type(bucketInput, 'test');
53-
const accessKeyIDInput = screen.getByLabelText('Access Key ID');
54-
await userEvent.type(accessKeyIDInput, 'Test');
55-
const secretAccessKeyInput = screen.getByLabelText('Secret Access Key');
56-
await userEvent.type(secretAccessKeyInput, 'Test');
57-
const logPathPrefixInput = screen.getByLabelText('Log Path Prefix');
58-
await userEvent.type(logPathPrefixInput, 'Test');
55+
it(
56+
'should render all inputs for Akamai Object Storage type and allow to fill out them',
57+
{ timeout: 10000 },
58+
async () => {
59+
renderDestinationCreate(flags, { label: '' });
60+
61+
const destinationNameInput = screen.getByLabelText('Destination Name');
62+
await userEvent.type(destinationNameInput, 'Test');
63+
const hostInput = screen.getByLabelText('Host');
64+
await userEvent.type(hostInput, 'test');
65+
const bucketInput = screen.getByLabelText('Bucket');
66+
await userEvent.type(bucketInput, 'test');
67+
const accessKeyIDInput = screen.getByLabelText('Access Key ID');
68+
await userEvent.type(accessKeyIDInput, 'Test');
69+
const secretAccessKeyInput = screen.getByLabelText('Secret Access Key');
70+
await userEvent.type(secretAccessKeyInput, 'Test');
71+
const logPathPrefixInput = screen.getByLabelText('Log Path Prefix');
72+
await userEvent.type(logPathPrefixInput, 'Test');
5973

60-
expect(destinationNameInput).toHaveValue('Test');
61-
expect(hostInput).toHaveValue('test');
62-
expect(bucketInput).toHaveValue('test');
63-
expect(accessKeyIDInput).toHaveValue('Test');
64-
expect(secretAccessKeyInput).toHaveValue('Test');
65-
expect(logPathPrefixInput).toHaveValue('Test');
66-
}
67-
);
68-
69-
it('should render Sample Destination Object Name and change its value according to Log Path Prefix input', async () => {
70-
const accountEuuid = 'XYZ-123';
71-
const [month, day, year] = new Date().toLocaleDateString().split('/');
72-
server.use(
73-
http.get('*/account', () => {
74-
return HttpResponse.json(accountFactory.build({ euuid: accountEuuid }));
75-
})
74+
expect(destinationNameInput).toHaveValue('Test');
75+
expect(hostInput).toHaveValue('test');
76+
expect(bucketInput).toHaveValue('test');
77+
expect(accessKeyIDInput).toHaveValue('Test');
78+
expect(secretAccessKeyInput).toHaveValue('Test');
79+
expect(logPathPrefixInput).toHaveValue('Test');
80+
}
7681
);
7782

78-
renderDestinationCreate();
83+
it('should render Sample Destination Object Name and change its value according to Log Path Prefix input', async () => {
84+
const accountEuuid = 'XYZ-123';
85+
const [month, day, year] = new Date().toLocaleDateString().split('/');
86+
server.use(
87+
http.get('*/account', () => {
88+
return HttpResponse.json(
89+
accountFactory.build({ euuid: accountEuuid })
90+
);
91+
})
92+
);
93+
94+
renderDestinationCreate(flags);
95+
96+
let samplePath;
97+
await waitFor(() => {
98+
samplePath = screen.getByText(
99+
`/audit_logs/com.akamai.audit/${accountEuuid}/${year}/${month}/${day}/akamai_log-000166-1756015362-319597-login.gz`
100+
);
101+
expect(samplePath).toBeInTheDocument();
102+
});
103+
// Type the test value inside the input
104+
const logPathPrefixInput = screen.getByLabelText('Log Path Prefix');
105+
106+
await userEvent.type(logPathPrefixInput, 'test');
107+
// sample path should be created based on *log path* value
108+
expect(samplePath!.textContent).toEqual(
109+
'/test/akamai_log-000166-1756015362-319597-login.gz'
110+
);
111+
112+
await userEvent.clear(logPathPrefixInput);
113+
await userEvent.type(logPathPrefixInput, '/test');
114+
expect(samplePath!.textContent).toEqual(
115+
'/test/akamai_log-000166-1756015362-319597-login.gz'
116+
);
79117

80-
let samplePath;
81-
await waitFor(() => {
82-
samplePath = screen.getByText(
83-
`/audit_logs/com.akamai.audit/${accountEuuid}/${year}/${month}/${day}/akamai_log-000166-1756015362-319597-login.gz`
118+
await userEvent.clear(logPathPrefixInput);
119+
await userEvent.type(logPathPrefixInput, '/');
120+
expect(samplePath!.textContent).toEqual(
121+
'/akamai_log-000166-1756015362-319597-login.gz'
84122
);
85-
expect(samplePath).toBeInTheDocument();
86123
});
87-
// Type the test value inside the input
88-
const logPathPrefixInput = screen.getByLabelText('Log Path Prefix');
124+
});
89125

90-
await userEvent.type(logPathPrefixInput, 'test');
91-
// sample path should be created based on *log path* value
92-
expect(samplePath!.textContent).toEqual(
93-
'/test/akamai_log-000166-1756015362-319597-login.gz'
94-
);
126+
describe('when customHttpsEnabled feature flag is set to true', () => {
127+
const flags = {
128+
aclpLogs: {
129+
enabled: true,
130+
beta: false,
131+
customHttpsEnabled: true,
132+
},
133+
};
95134

96-
await userEvent.clear(logPathPrefixInput);
97-
await userEvent.type(logPathPrefixInput, '/test');
98-
expect(samplePath!.textContent).toEqual(
99-
'/test/akamai_log-000166-1756015362-319597-login.gz'
100-
);
135+
it('should render enabled Destination Type input with Akamai Object Storage selected and allow to select Custom HTTPS', async () => {
136+
renderDestinationCreate(flags);
137+
138+
const destinationTypeAutocomplete =
139+
screen.getByLabelText('Destination Type');
101140

102-
await userEvent.clear(logPathPrefixInput);
103-
await userEvent.type(logPathPrefixInput, '/');
104-
expect(samplePath!.textContent).toEqual(
105-
'/akamai_log-000166-1756015362-319597-login.gz'
141+
expect(destinationTypeAutocomplete).toBeEnabled();
142+
expect(destinationTypeAutocomplete).toHaveValue('Akamai Object Storage');
143+
await userEvent.click(destinationTypeAutocomplete);
144+
const customHttpsOption = await screen.findByText('Custom HTTPS');
145+
await userEvent.click(customHttpsOption);
146+
expect(destinationTypeAutocomplete).toHaveValue('Custom HTTPS');
147+
});
148+
149+
it(
150+
'should render all inputs for Custom HTTPS type and allow to fill them out',
151+
{ timeout: 10000 },
152+
async () => {
153+
renderDestinationCreate(flags, { label: '' });
154+
155+
const destinationTypeAutocomplete =
156+
screen.getByLabelText('Destination Type');
157+
await userEvent.click(destinationTypeAutocomplete);
158+
const customHttpsOption = await screen.findByText('Custom HTTPS');
159+
await userEvent.click(customHttpsOption);
160+
expect(destinationTypeAutocomplete).toHaveValue('Custom HTTPS');
161+
162+
const destinationNameInput = screen.getByLabelText('Destination Name');
163+
await userEvent.type(destinationNameInput, 'Test');
164+
165+
// With None Authentication type selected, the Username and Password inputs should not be rendered
166+
const notYetExistingUsernameInput = screen.queryByLabelText('Username');
167+
expect(notYetExistingUsernameInput).not.toBeInTheDocument();
168+
const notYetExistingPasswordInput = screen.queryByLabelText('Password');
169+
expect(notYetExistingPasswordInput).not.toBeInTheDocument();
170+
171+
// Open Authentication select and choose Basic option
172+
const authenticationAutocomplete =
173+
screen.getByLabelText('Authentication');
174+
expect(authenticationAutocomplete).toHaveValue('None');
175+
await userEvent.click(authenticationAutocomplete);
176+
const basicAuthentication = await screen.findByText('Basic');
177+
await userEvent.click(basicAuthentication);
178+
expect(authenticationAutocomplete).toHaveValue('Basic');
179+
180+
// With Authentication type set to Basic, the Username and Password inputs should be rendered
181+
const usernameInput = screen.getByLabelText('Username');
182+
await userEvent.type(usernameInput, 'Username test');
183+
expect(usernameInput.getAttribute('value')).toEqual('Username test');
184+
185+
const passwordInput = screen.getByLabelText('Password');
186+
await userEvent.type(passwordInput, 'Password test');
187+
expect(passwordInput.getAttribute('value')).toEqual('Password test');
188+
189+
// Endpoint URL
190+
const endpointUrlInput = screen.getByLabelText('Endpoint URL');
191+
await userEvent.type(endpointUrlInput, 'Endpoint URL test');
192+
expect(endpointUrlInput.getAttribute('value')).toEqual(
193+
'Endpoint URL test'
194+
);
195+
}
106196
);
107197
});
108198

109199
describe('given Test Connection and Create Destination buttons', () => {
200+
const flags = {
201+
aclpLogs: {
202+
enabled: true,
203+
beta: false,
204+
customHttpsEnabled: false,
205+
},
206+
};
110207
const testConnectionButtonText = 'Test Connection';
111208
const createDestinationButtonText = 'Create Destination';
112209

@@ -144,7 +241,7 @@ describe('DestinationCreate', () => {
144241
})
145242
);
146243

147-
renderDestinationCreate();
244+
renderDestinationCreate(flags);
148245

149246
const testConnectionButton = screen.getByRole('button', {
150247
name: testConnectionButtonText,
@@ -181,7 +278,7 @@ describe('DestinationCreate', () => {
181278
})
182279
);
183280

184-
renderDestinationCreate();
281+
renderDestinationCreate(flags);
185282

186283
const testConnectionButton = screen.getByRole('button', {
187284
name: testConnectionButtonText,

packages/manager/src/features/Delivery/Destinations/DestinationForm/DestinationForm.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { destinationType } from '@linode/api-v4';
1+
import { authenticationType, destinationType } from '@linode/api-v4';
22
import { Autocomplete, Paper, TextField } from '@linode/ui';
33
import { capitalize, scrollErrorIntoViewV2 } from '@linode/utilities';
44
import Grid from '@mui/material/Grid';
@@ -8,8 +8,12 @@ import type { SubmitHandler } from 'react-hook-form';
88
import { useFormContext } from 'react-hook-form';
99
import { Controller, useWatch } from 'react-hook-form';
1010

11-
import { getDestinationTypeOption } from 'src/features/Delivery/deliveryUtils';
11+
import {
12+
getDestinationTypeOption,
13+
useIsACLPLogsEnabled,
14+
} from 'src/features/Delivery/deliveryUtils';
1215
import { DestinationAkamaiObjectStorageDetailsForm } from 'src/features/Delivery/Shared/DestinationAkamaiObjectStorageDetailsForm';
16+
import { DestinationCustomHttpsDetailsForm } from 'src/features/Delivery/Shared/DestinationCustomHttpsDetailsForm';
1317
import { FormSubmitBar } from 'src/features/Delivery/Shared/FormSubmitBar/FormSubmitBar';
1418
import { destinationTypeOptions } from 'src/features/Delivery/Shared/types';
1519
import { useVerifyDestination } from 'src/features/Delivery/Shared/useVerifyDestination';
@@ -28,6 +32,7 @@ interface DestinationFormProps {
2832
export const DestinationForm = (props: DestinationFormProps) => {
2933
const { mode, isSubmitting, onSubmit } = props;
3034

35+
const { isACLPLogsCustomHttpsEnabled } = useIsACLPLogsEnabled();
3136
const {
3237
verifyDestination,
3338
isPending: isVerifyingDestination,
@@ -36,7 +41,8 @@ export const DestinationForm = (props: DestinationFormProps) => {
3641
} = useVerifyDestination();
3742

3843
const formRef = React.useRef<HTMLFormElement>(null);
39-
const { control, handleSubmit } = useFormContext<DestinationFormType>();
44+
const { control, handleSubmit, setValue } =
45+
useFormContext<DestinationFormType>();
4046
const destination = useWatch({
4147
control,
4248
}) as DestinationFormType;
@@ -56,10 +62,16 @@ export const DestinationForm = (props: DestinationFormProps) => {
5662
render={({ field }) => (
5763
<Autocomplete
5864
disableClearable
59-
disabled
65+
disabled={!isACLPLogsCustomHttpsEnabled}
6066
label="Destination Type"
6167
onBlur={field.onBlur}
6268
onChange={(_, { value }) => {
69+
if (value === destinationType.CustomHttps) {
70+
setValue(
71+
'details.authentication.type',
72+
authenticationType.None
73+
);
74+
}
6375
field.onChange(value);
6476
}}
6577
options={destinationTypeOptions}
@@ -98,6 +110,13 @@ export const DestinationForm = (props: DestinationFormProps) => {
98110
mode={mode}
99111
/>
100112
)}
113+
{isACLPLogsCustomHttpsEnabled &&
114+
destination.type === destinationType.CustomHttps && (
115+
<DestinationCustomHttpsDetailsForm
116+
entity="destination"
117+
mode={mode}
118+
/>
119+
)}
101120
</Paper>
102121
</Grid>
103122
<Grid size={{ lg: 3, md: 12, sm: 12, xs: 12 }}>

0 commit comments

Comments
 (0)