Skip to content
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
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-13380-changed-1770626350671.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Changed
---

Delivery Logs - adjust DestinationDetailsPayload type for Custom HTTPS destinations ([#13380](https://github.com/linode/manager/pull/13380))
13 changes: 11 additions & 2 deletions packages/api-v4/src/delivery/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ export interface CustomHTTPSDetails {
endpoint_url: string;
}

export interface CustomHTTPSDetailsExtended extends CustomHTTPSDetails {
authentication: Authentication & {
details?: AuthenticationDetailsExtended;
};
}

interface ClientCertificateDetails {
client_ca_certificate?: string;
client_certificate?: string;
Expand All @@ -117,10 +123,13 @@ interface Authentication {
}

interface AuthenticationDetails {
basic_authentication_password: string;
basic_authentication_user: string;
}

interface AuthenticationDetailsExtended extends AuthenticationDetails {
basic_authentication_password: string;
}

export interface CustomHeader {
name: string;
value: string;
Expand Down Expand Up @@ -152,7 +161,7 @@ export interface AkamaiObjectStorageDetailsPayload

export type DestinationDetailsPayload =
| AkamaiObjectStorageDetailsPayload
| CustomHTTPSDetails;
| CustomHTTPSDetailsExtended;

export interface CreateDestinationPayload {
details: DestinationDetailsPayload;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Delivery Logs - selected destination summary in a Create Stream form for Custom HTTPS destinations, edit Custom HTTPS destination ([#13380](https://github.com/linode/manager/pull/13380))
119 changes: 78 additions & 41 deletions packages/manager/src/features/Delivery/Shared/LabelValue.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,108 @@
import { Box, Tooltip, Typography } from '@linode/ui';
import { styled, useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';

const maxWidth = 416;
const labelWidth = 160;
const valueWidth = maxWidth - labelWidth;

interface LabelValueProps {
children?: React.ReactNode;
compact?: boolean;
'data-testid'?: string;
label: string;
smHideTooltip?: boolean;
value: string;
}

export const LabelValue = (props: LabelValueProps) => {
const {
compact = false,
label,
value,
'data-testid': dataTestId,
children,
smHideTooltip,
} = props;
const { label, value, 'data-testid': dataTestId } = props;
const theme = useTheme();
const matchesSmDown = useMediaQuery(theme.breakpoints.down('sm'));
const labelRef = useRef<HTMLDivElement>(null);
const [isLabelOverflowing, setIsLabelOverflowing] = useState(false);
const valueRef = useRef<HTMLDivElement>(null);
const [isValueOverflowing, setIsValueOverflowing] = useState(false);

useEffect(() => {
const checkLabelOverflow = () => {
if (labelRef.current) {
setIsLabelOverflowing(
labelRef.current.scrollWidth > labelRef.current.clientWidth
);
}
};
const checkValueOverflow = () => {
if (valueRef.current) {
setIsValueOverflowing(
valueRef.current.scrollWidth > valueRef.current.clientWidth
);
}
};
checkLabelOverflow();
checkValueOverflow();
});

return (
<Box
alignItems="center"
display="flex"
justifyContent="space-between"
marginTop={theme.spacingFunction(16)}
width={maxWidth}
>
<Typography
sx={{
mr: 1,
font: theme.font.bold,
width: compact ? 'auto' : 160,
}}
>
{label}:
</Typography>
<StyledValue
data-testid={dataTestId}
title={!smHideTooltip && matchesSmDown ? value : undefined}
>
<Typography>{value}</Typography>
</StyledValue>
{children}
<StyledLabel title={isLabelOverflowing ? label : undefined}>
<Box alignItems="center" display="flex" maxWidth={labelWidth}>
<Typography
ref={labelRef}
sx={{
font: theme.font.bold,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{label}
</Typography>
<Typography sx={{ font: theme.font.bold, flexShrink: 0 }}>
:
</Typography>
</Box>
</StyledLabel>
<Box width={valueWidth}>
<StyledValue
data-testid={dataTestId}
title={isValueOverflowing ? value : undefined}
>
<Typography
ref={valueRef}
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
width: 'fit-content',
}}
>
{value}
</Typography>
</StyledValue>
</Box>
</Box>
);
};

const StyledValue = styled(Tooltip, {
label: 'StyledValue',
})(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.tokens.alias.Interaction.Background.Disabled,
border: `1px solid ${theme.tokens.alias.Border.Neutral}`,
borderRadius: 4,
display: 'flex',
height: theme.spacingFunction(24),
padding: theme.spacingFunction(4, 8),
[theme.breakpoints.down('sm')]: {
display: 'block',
maxWidth: '174px',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
padding: theme.spacingFunction(1, 8),
},
lineHeight: theme.spacingFunction(24),
maxWidth: valueWidth,
padding: theme.spacingFunction(1, 8),
}));

const StyledLabel = styled(Tooltip, {
label: 'StyledLabel',
})(({ theme }) => ({
height: theme.spacingFunction(24),
lineHeight: theme.spacingFunction(24),
maxWidth: labelWidth,
}));
4 changes: 2 additions & 2 deletions packages/manager/src/features/Delivery/Shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import type {
AkamaiObjectStorageDetailsExtended,
CreateDestinationPayload,
CustomHTTPSDetails,
CustomHTTPSDetailsExtended,
} from '@linode/api-v4';

export type FormMode = 'create' | 'edit';
Expand Down Expand Up @@ -85,7 +85,7 @@ export const contentTypeOptions: AutocompleteOption[] = [

export type DestinationDetailsForm =
| AkamaiObjectStorageDetailsExtended
| CustomHTTPSDetails;
| CustomHTTPSDetailsExtended;

export interface DestinationForm
extends Omit<CreateDestinationPayload, 'details'> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@ export const DestinationAkamaiObjectStorageDetailsSummary = (
<LabelValue
data-testid="access-key-id"
label="Access Key ID"
smHideTooltip={true}
value="*****************"
/>
<LabelValue
data-testid="secret-access-key"
label="Secret Access Key"
smHideTooltip={true}
value="*****************"
/>
{!!path && <LabelValue label="Log Path" value={path} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { dataCompressionType } from '@linode/api-v4';
import { screen } from '@testing-library/react';
import React from 'react';
import { expect } from 'vitest';

import { renderWithTheme } from 'src/utilities/testHelpers';

import { DestinationCustomHTTPSDetailsSummary } from './DestinationCustomHTTPSDetailsSummary';

import type { CustomHTTPSDetails } from '@linode/api-v4';

describe('DestinationCustomHTTPSDetailsSummary', () => {
it('renders basic authentication details correctly', () => {
const details: CustomHTTPSDetails = {
authentication: {
type: 'basic',
details: {
basic_authentication_user: 'testuser',
},
},
endpoint_url: 'https://example.com/',

Check warning on line 21 in packages/manager/src/features/Delivery/Streams/StreamForm/Delivery/DestinationCustomHTTPSDetailsSummary.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 8 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 8 times.","line":21,"column":21,"nodeType":"Literal","endLine":21,"endColumn":43}
data_compression: dataCompressionType.Gzip,
};

renderWithTheme(<DestinationCustomHTTPSDetailsSummary {...details} />);

// Authentication:
expect(screen.getByText('basic')).toBeVisible();
// Endpoint URL:
expect(screen.getByText('https://example.com/')).toBeVisible();
// Username:
expect(screen.getByText('testuser')).toBeVisible();
// Password:
expect(screen.getByTestId('password')).toHaveTextContent(
'*****************'
);
});

it('renders none authentication without username and password', () => {
const details: CustomHTTPSDetails = {
authentication: {
type: 'none',
},
endpoint_url: 'https://example.com/',
data_compression: dataCompressionType.Gzip,
};

renderWithTheme(<DestinationCustomHTTPSDetailsSummary {...details} />);

// Authentication:
expect(screen.getByText('none')).toBeVisible();
// Endpoint URL:
expect(screen.getByText('https://example.com/')).toBeVisible();
// Username:
expect(screen.queryByText('Username')).not.toBeInTheDocument();
// Password:
expect(screen.queryByTestId('password')).not.toBeInTheDocument();
});

it('renders client certificate details when provided', () => {
const details: CustomHTTPSDetails = {
authentication: { type: 'none' },
endpoint_url: 'https://example.com/',
client_certificate_details: {
tls_hostname: 'tls.example.com',
client_ca_certificate: 'ca-cert-content',
client_certificate: 'client-cert-content',
client_private_key: 'private-key-content',
},
data_compression: dataCompressionType.Gzip,
};

renderWithTheme(<DestinationCustomHTTPSDetailsSummary {...details} />);

expect(screen.getByText('Additional Options')).toBeVisible();
expect(screen.queryByTestId('client-certificate-header')).toBeVisible();
// TLS Hostname:
expect(screen.getByText('tls.example.com')).toBeVisible();
// CA Certificate:
expect(screen.getByText('ca-cert-content')).toBeVisible();
// Client Certificate:
expect(screen.getByText('client-cert-content')).toBeVisible();
// Client Key:
expect(screen.getByText('private-key-content')).toBeVisible();
});

it('renders content type when provided', () => {
const details: CustomHTTPSDetails = {
authentication: { type: 'none' },
endpoint_url: 'https://example.com/',
content_type: 'application/json',
data_compression: dataCompressionType.Gzip,
};

renderWithTheme(<DestinationCustomHTTPSDetailsSummary {...details} />);

expect(screen.getByText('HTTPS Headers')).toBeVisible();

Check warning on line 97 in packages/manager/src/features/Delivery/Streams/StreamForm/Delivery/DestinationCustomHTTPSDetailsSummary.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 3 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 3 times.","line":97,"column":29,"nodeType":"Literal","endLine":97,"endColumn":44}
expect(screen.getByText('application/json')).toBeVisible();
});

it('renders custom headers when provided', () => {
const details: CustomHTTPSDetails = {
authentication: { type: 'none' },
endpoint_url: 'https://example.com/',
custom_headers: [
{ name: 'X-Custom-Header', value: 'custom-value' },
{ name: 'Authorization', value: 'Bearer token123' },
],
data_compression: dataCompressionType.Gzip,
};

renderWithTheme(<DestinationCustomHTTPSDetailsSummary {...details} />);

expect(screen.getByText('HTTPS Headers')).toBeVisible();
// Custom Header 1:
expect(screen.getByText('X-Custom-Header')).toBeVisible();
expect(screen.getByText('custom-value')).toBeVisible();
// Custom Header 2:
expect(screen.getByText('Authorization')).toBeVisible();
expect(screen.getByText('Bearer token123')).toBeVisible();
});

it('does not render Additional Options section when no optional fields provided', () => {
const details: CustomHTTPSDetails = {
authentication: { type: 'none' },
endpoint_url: 'https://example.com/',
data_compression: dataCompressionType.Gzip,
};

renderWithTheme(<DestinationCustomHTTPSDetailsSummary {...details} />);

expect(screen.queryByText('Additional Options')).not.toBeInTheDocument();
expect(screen.queryByText('HTTPS Headers')).not.toBeInTheDocument();
});
});
Loading