Skip to content
Merged
27 changes: 25 additions & 2 deletions cypress/integration/snapshotCreation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ describe('test snapshot creation', () => {
cy.intercept({ method: 'GET', url: 'api/resources/v1/profiles/**' }).as(
'getBillingProfileById',
);
cy.intercept(
{ method: 'GET', url: 'api/resources/v1/profiles?*' },
{
statusCode: 200,
body: {
total: 1,
items: [
{
id: 'profileId',
profileName: 'Test Billing Profile',
description: 'Test profile for E2E tests',
billingAccountId: 'ba-test-001',
},
],
},
},
).as('getBillingProfiles');
cy.intercept('POST', '/api/repository/v1/snapshots', {
statusCode: 200,
body: {
Expand Down Expand Up @@ -84,9 +101,15 @@ describe('test snapshot creation is disabled', () => {
beforeEach(() => {
cy.intercept('GET', 'api/repository/v1/datasets/**').as('getDataset');
cy.intercept('GET', 'api/repository/v1/datasets/**/policies').as('getDatasetPolicies');
cy.intercept('GET', '/api/resources/v1/profiles?*', {
statusCode: 401,
body: {
message: 'unauthorized',
},
});
cy.intercept('GET', '/api/resources/v1/profiles/**', {
status: 401,
response: {
statusCode: 401,
body: {
message: 'unauthorized',
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { BillingProfileModel } from 'generated/tdr';
import { Box, FormLabel, TextField, Typography } from '@mui/material';
import JadeDropdown from 'components/dataset/data/JadeDropdown';
import { uniq } from 'lodash';
import { FormLabel, TextField, Typography } from '@mui/material';
import BillingProfileDropdown from 'components/dataset/data/sidebar/panels/BillingProfileDropdown';
import React from 'react';

export interface FullViewSnapshotDetailsProps {
Expand Down Expand Up @@ -49,35 +48,15 @@ export function FullViewSnapshotDetails(props: FullViewSnapshotDetailsProps) {
onChange={(e) => setSnapshotDescription(e.target.value)}
/>
<Typography sx={{ color: 'black' }}>
Do you want to use the Google Billing Project associated with this dataset or would you like
to select a different one?
Do you want to use the TDR Billing Profile associated with this dataset or would you like to
select a different one?
</Typography>
<Box sx={{ marginTop: '8px' }}>
<FormLabel
sx={{ fontWeight: 600, color: 'black' }}
htmlFor="billing-profile-select"
required
>
Google Billing Project
</FormLabel>
</Box>
<JadeDropdown
sx={{ height: '2.5rem' }}
disabled={billingProfiles.length <= 1}
options={uniq(
billingProfiles
.filter((billingProfile) => billingProfile.profileName !== undefined)
.map((billingProfile) => billingProfile.profileName) as string[],
)}
name="billing-profile"
onSelectedItem={(event) =>
setSelectedBillingProfile(
billingProfiles.find(
(billingProfile) => billingProfile.profileName === event.target.value,
),
)
}
value={selectedBillingProfile?.profileName || ''}
<BillingProfileDropdown
billingProfiles={billingProfiles}
selectedBillingProfile={selectedBillingProfile}
onSelectedItem={setSelectedBillingProfile}
sx={{ height: '2.5rem', marginTop: '8px' }}
labelProps={{ sx: { fontWeight: 600, color: 'black' } }}
/>
</>
);
Expand Down
17 changes: 12 additions & 5 deletions src/components/dataset/data/DatasetDataView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,18 @@ type IProps = {
dataset: DatasetModel;
dispatch: Dispatch<Action>;
polling: boolean;
profile: BillingProfileModel;
billingProfiles: BillingProfileModel[];
snapshotRequest: SnapshotRequest;
} & RouteComponentProps<{ uuid?: string }>;

function DatasetDataView({ dataset, dispatch, match, polling, profile, snapshotRequest }: IProps) {
function DatasetDataView({
dataset,
dispatch,
match,
polling,
billingProfiles,
snapshotRequest,
}: IProps) {
const [selected, setSelected] = useState('');
const [selectedTable, setSelectedTable] = useState<TableModel | undefined>(undefined);
const [sidebarWidth, setSidebarWidth] = useState(0);
Expand Down Expand Up @@ -105,10 +112,10 @@ function DatasetDataView({ dataset, dispatch, match, polling, profile, snapshotR
}, [datasetLoaded, dataset, selectedTable, canLink, snapshotRequest.assetName]);

useEffect(() => {
if (profile.id) {
if (billingProfiles && billingProfiles.length > 0) {
setCanLink(true);
}
}, [profile]);
}, [billingProfiles]);

const handleEnumeration = (
_limit: number,
Expand Down Expand Up @@ -174,7 +181,7 @@ function mapStateToProps(state: TdrState) {
return {
dataset: state.datasets.dataset,
polling: state.query.polling,
profile: state.profiles.profile,
billingProfiles: state.profiles.profiles,
snapshotRequest: state.snapshots.snapshotRequest,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { uniq } from 'lodash';
import { Typography } from '@mui/material';
import JadeDropdown from '../../JadeDropdown';

function BillingProfileDropdown({
billingProfiles,
selectedBillingProfile,
onSelectedItem,
disabled = false,
sx = { height: '2.5rem', marginTop: '8px' },
showLabel = true,
labelProps = { variant: 'subtitle2', marginTop: 1 },
}) {
const handleItemSelection = (event) => {
const selectedProfile = billingProfiles.find(
(billingProfile) => billingProfile.profileName === event.target.value,
);
onSelectedItem(selectedProfile);
};

const profileOptions = uniq(
billingProfiles
.filter((billingProfile) => billingProfile.profileName !== undefined)
.map((billingProfile) => billingProfile.profileName),
);

return (
<>
{showLabel && <Typography {...labelProps}>Billing Profile</Typography>}
<JadeDropdown
sx={sx}
disabled={disabled || billingProfiles.length <= 1}
options={profileOptions}
name="billing-profile"
onSelectedItem={handleItemSelection}
value={selectedBillingProfile?.profileName || ''}
data-cy="selectBillingProfile"
/>
</>
);
}

BillingProfileDropdown.propTypes = {
billingProfiles: PropTypes.array.isRequired,
disabled: PropTypes.bool,
labelProps: PropTypes.object,
onSelectedItem: PropTypes.func.isRequired,
selectedBillingProfile: PropTypes.object,
showLabel: PropTypes.bool,
sx: PropTypes.object,
};

export default BillingProfileDropdown;
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { mount } from 'cypress/react';
import { ThemeProvider } from '@mui/material/styles';
import React from 'react';
import globalTheme from 'modules/theme';
import BillingProfileDropdown from './BillingProfileDropdown';

interface BillingProfile {
id: string;
profileName?: string;
[key: string]: any;
}

const mockBillingProfiles: BillingProfile[] = [
{
id: 'profile-1',
profileName: 'Default Profile',
},
{
id: 'profile-2',
profileName: 'Alternative Profile',
},
{
id: 'profile-3',
profileName: 'Test Profile',
},
];

const setUp = (overrideProps = {}) => {
const props = {
billingProfiles: mockBillingProfiles,
selectedBillingProfile: mockBillingProfiles[0],
onSelectedItem: cy.stub(),
...overrideProps,
};

mount(
<ThemeProvider theme={globalTheme}>
<BillingProfileDropdown {...props} />
</ThemeProvider>,
);

return props;
};

describe('BillingProfileDropdown Component', () => {
// No need for beforeEach since we create fresh stubs in each test

describe('Rendering', () => {
it('should render the billing profile label by default', () => {
setUp();

cy.contains('Billing Profile').should('be.visible');
});

it('should not render label when showLabel is false', () => {
setUp({ showLabel: false });

cy.contains('Billing Profile').should('not.exist');
});

it('should render dropdown with correct initial value', () => {
setUp();

cy.get('[data-cy="billingProfile"]').should('contain', 'Default Profile');
});
});

describe('Profile Selection', () => {
it('should display all available billing profiles', () => {
setUp();

cy.get('[data-cy="billingProfile"]').click();
cy.contains('Default Profile').should('exist');
cy.contains('Alternative Profile').should('exist');
cy.contains('Test Profile').should('exist');
});

it('should call onSelectedItem when profile is selected', () => {
const props = setUp();

cy.get('[data-cy="billingProfile"]').click();
cy.contains('Alternative Profile').click();

cy.wrap(props.onSelectedItem).should('have.been.calledOnce');
cy.wrap(props.onSelectedItem).should('have.been.calledWith', mockBillingProfiles[1]);
});

it('should filter out profiles without profileName', () => {
const profilesWithUndefined = [
...mockBillingProfiles,
{ id: 'profile-4' }, // Missing profileName
{ id: 'profile-5', profileName: undefined },
];

setUp({ billingProfiles: profilesWithUndefined });

cy.get('[data-cy="billingProfile"]').click();
cy.contains('Default Profile').should('exist');
cy.contains('Alternative Profile').should('exist');
cy.contains('Test Profile').should('exist');
// Profiles without profileName should not appear
cy.get('[data-cy="menuItem-undefined"]').should('not.exist');
});
});

describe('Disabled State', () => {
it('should be disabled when only one profile is available', () => {
setUp({ billingProfiles: [mockBillingProfiles[0]] });

cy.get('[data-cy="billingProfile"]').should('have.class', 'Mui-disabled');
});

it('should be disabled when disabled prop is true', () => {
setUp({ disabled: true });

cy.get('[data-cy="billingProfile"]').should('have.class', 'Mui-disabled');
});

it('should be enabled when multiple profiles available and not explicitly disabled', () => {
setUp();

cy.get('[data-cy="billingProfile"]').should('not.have.attr', 'aria-disabled');
});
});

describe('Empty State', () => {
it('should handle empty billing profiles array', () => {
setUp({ billingProfiles: [], selectedBillingProfile: null });

cy.get('[data-cy="billingProfile"]').should('have.value', '');
cy.get('[data-cy="billingProfile"]').should('have.class', 'Mui-disabled');
});

it('should handle null selectedBillingProfile', () => {
setUp({ selectedBillingProfile: null });

cy.get('[data-cy="billingProfile"]').should('have.value', '');
});
});

describe('Styling and Props', () => {
it('should apply custom sx props', () => {
const customSx = { height: '3rem', marginTop: '16px', backgroundColor: 'red' };
setUp({ sx: customSx });

cy.get('[data-cy="billingProfile"]').should('exist');
// Note: Testing exact styles is complex in Cypress, but we can verify the component renders
});

it('should handle duplicate profile names correctly', () => {
const duplicateProfiles = [
{ id: 'profile-1', profileName: 'Same Name' },
{ id: 'profile-2', profileName: 'Same Name' },
{ id: 'profile-3', profileName: 'Different Name' },
];

setUp({ billingProfiles: duplicateProfiles });

cy.get('[data-cy="billingProfile"]').click();
// Should only show unique profile names
cy.contains('Same Name').should('exist');
cy.contains('Different Name').should('exist');
});
});

describe('Integration', () => {
it('should work with real-world profile data structure', () => {
it('should call onSelectedItem with the correct profile when selection changes', () => {
const realWorldProfiles = [
{
id: 'bp-12345',
profileName: 'Production Environment',
billingAccountId: 'ba-prod-001',
description: 'Production billing profile for live workloads',
},
{
id: 'bp-67890',
profileName: 'Development Environment',
billingAccountId: 'ba-dev-001',
description: 'Development billing profile for testing',
},
];

const props = setUp({
billingProfiles: realWorldProfiles,
selectedBillingProfile: realWorldProfiles[0],
});

cy.get('[data-cy="billingProfile"]').should('contain', 'Production Environment');

cy.get('[data-cy="billingProfile"]').click();
cy.contains('Development Environment').click();

cy.wrap(props.onSelectedItem).should('have.been.calledOnce');
cy.wrap(props.onSelectedItem.getCall(0).args[0])
.should('have.property', 'id', 'bp-67890')
.should('have.property', 'profileName', 'Development Environment');
});
});
});
});
Loading
Loading