Skip to content

Commit af17097

Browse files
rjohanekraejohanek
andauthored
[DC-1804] TDR UI add select billing profile to filtered snapshot create flow (#1841)
Co-authored-by: Rae Johanek <[email protected]>
1 parent 08fc8a1 commit af17097

File tree

10 files changed

+913
-45
lines changed

10 files changed

+913
-45
lines changed

cypress/integration/snapshotCreation.spec.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ describe('test snapshot creation', () => {
55
cy.intercept({ method: 'GET', url: 'api/resources/v1/profiles/**' }).as(
66
'getBillingProfileById',
77
);
8+
cy.intercept(
9+
{ method: 'GET', url: 'api/resources/v1/profiles?*' },
10+
{
11+
statusCode: 200,
12+
body: {
13+
total: 1,
14+
items: [
15+
{
16+
id: 'profileId',
17+
profileName: 'Test Billing Profile',
18+
description: 'Test profile for E2E tests',
19+
billingAccountId: 'ba-test-001',
20+
},
21+
],
22+
},
23+
},
24+
).as('getBillingProfiles');
825
cy.intercept('POST', '/api/repository/v1/snapshots', {
926
statusCode: 200,
1027
body: {
@@ -84,9 +101,15 @@ describe('test snapshot creation is disabled', () => {
84101
beforeEach(() => {
85102
cy.intercept('GET', 'api/repository/v1/datasets/**').as('getDataset');
86103
cy.intercept('GET', 'api/repository/v1/datasets/**/policies').as('getDatasetPolicies');
104+
cy.intercept('GET', '/api/resources/v1/profiles?*', {
105+
statusCode: 401,
106+
body: {
107+
message: 'unauthorized',
108+
},
109+
});
87110
cy.intercept('GET', '/api/resources/v1/profiles/**', {
88-
status: 401,
89-
response: {
111+
statusCode: 401,
112+
body: {
90113
message: 'unauthorized',
91114
},
92115
});

src/components/common/FullViewSnapshotModal/FullViewSnapshotDetails.tsx

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { BillingProfileModel } from 'generated/tdr';
2-
import { Box, FormLabel, TextField, Typography } from '@mui/material';
3-
import JadeDropdown from 'components/dataset/data/JadeDropdown';
4-
import { uniq } from 'lodash';
2+
import { FormLabel, TextField, Typography } from '@mui/material';
3+
import BillingProfileDropdown from 'components/dataset/data/sidebar/panels/BillingProfileDropdown';
54
import React from 'react';
65

76
export interface FullViewSnapshotDetailsProps {
@@ -49,35 +48,15 @@ export function FullViewSnapshotDetails(props: FullViewSnapshotDetailsProps) {
4948
onChange={(e) => setSnapshotDescription(e.target.value)}
5049
/>
5150
<Typography sx={{ color: 'black' }}>
52-
Do you want to use the Google Billing Project associated with this dataset or would you like
53-
to select a different one?
51+
Do you want to use the TDR Billing Profile associated with this dataset or would you like to
52+
select a different one?
5453
</Typography>
55-
<Box sx={{ marginTop: '8px' }}>
56-
<FormLabel
57-
sx={{ fontWeight: 600, color: 'black' }}
58-
htmlFor="billing-profile-select"
59-
required
60-
>
61-
Google Billing Project
62-
</FormLabel>
63-
</Box>
64-
<JadeDropdown
65-
sx={{ height: '2.5rem' }}
66-
disabled={billingProfiles.length <= 1}
67-
options={uniq(
68-
billingProfiles
69-
.filter((billingProfile) => billingProfile.profileName !== undefined)
70-
.map((billingProfile) => billingProfile.profileName) as string[],
71-
)}
72-
name="billing-profile"
73-
onSelectedItem={(event) =>
74-
setSelectedBillingProfile(
75-
billingProfiles.find(
76-
(billingProfile) => billingProfile.profileName === event.target.value,
77-
),
78-
)
79-
}
80-
value={selectedBillingProfile?.profileName || ''}
54+
<BillingProfileDropdown
55+
billingProfiles={billingProfiles}
56+
selectedBillingProfile={selectedBillingProfile}
57+
onSelectedItem={setSelectedBillingProfile}
58+
sx={{ height: '2.5rem', marginTop: '8px' }}
59+
labelProps={{ sx: { fontWeight: 600, color: 'black' } }}
8160
/>
8261
</>
8362
);

src/components/dataset/data/DatasetDataView.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,18 @@ type IProps = {
2828
dataset: DatasetModel;
2929
dispatch: Dispatch<Action>;
3030
polling: boolean;
31-
profile: BillingProfileModel;
31+
billingProfiles: BillingProfileModel[];
3232
snapshotRequest: SnapshotRequest;
3333
} & RouteComponentProps<{ uuid?: string }>;
3434

35-
function DatasetDataView({ dataset, dispatch, match, polling, profile, snapshotRequest }: IProps) {
35+
function DatasetDataView({
36+
dataset,
37+
dispatch,
38+
match,
39+
polling,
40+
billingProfiles,
41+
snapshotRequest,
42+
}: IProps) {
3643
const [selected, setSelected] = useState('');
3744
const [selectedTable, setSelectedTable] = useState<TableModel | undefined>(undefined);
3845
const [sidebarWidth, setSidebarWidth] = useState(0);
@@ -105,10 +112,10 @@ function DatasetDataView({ dataset, dispatch, match, polling, profile, snapshotR
105112
}, [datasetLoaded, dataset, selectedTable, canLink, snapshotRequest.assetName]);
106113

107114
useEffect(() => {
108-
if (profile.id) {
115+
if (billingProfiles && billingProfiles.length > 0) {
109116
setCanLink(true);
110117
}
111-
}, [profile]);
118+
}, [billingProfiles]);
112119

113120
const handleEnumeration = (
114121
_limit: number,
@@ -174,7 +181,7 @@ function mapStateToProps(state: TdrState) {
174181
return {
175182
dataset: state.datasets.dataset,
176183
polling: state.query.polling,
177-
profile: state.profiles.profile,
184+
billingProfiles: state.profiles.profiles,
178185
snapshotRequest: state.snapshots.snapshotRequest,
179186
};
180187
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { uniq } from 'lodash';
4+
import { Typography } from '@mui/material';
5+
import JadeDropdown from '../../JadeDropdown';
6+
7+
function BillingProfileDropdown({
8+
billingProfiles,
9+
selectedBillingProfile,
10+
onSelectedItem,
11+
disabled = false,
12+
sx = { height: '2.5rem', marginTop: '8px' },
13+
showLabel = true,
14+
labelProps = { variant: 'subtitle2', marginTop: 1 },
15+
}) {
16+
const handleItemSelection = (event) => {
17+
const selectedProfile = billingProfiles.find(
18+
(billingProfile) => billingProfile.profileName === event.target.value,
19+
);
20+
onSelectedItem(selectedProfile);
21+
};
22+
23+
const profileOptions = uniq(
24+
billingProfiles
25+
.filter((billingProfile) => billingProfile.profileName !== undefined)
26+
.map((billingProfile) => billingProfile.profileName),
27+
);
28+
29+
return (
30+
<>
31+
{showLabel && <Typography {...labelProps}>Billing Profile</Typography>}
32+
<JadeDropdown
33+
sx={sx}
34+
disabled={disabled || billingProfiles.length <= 1}
35+
options={profileOptions}
36+
name="billing-profile"
37+
onSelectedItem={handleItemSelection}
38+
value={selectedBillingProfile?.profileName || ''}
39+
data-cy="selectBillingProfile"
40+
/>
41+
</>
42+
);
43+
}
44+
45+
BillingProfileDropdown.propTypes = {
46+
billingProfiles: PropTypes.array.isRequired,
47+
disabled: PropTypes.bool,
48+
labelProps: PropTypes.object,
49+
onSelectedItem: PropTypes.func.isRequired,
50+
selectedBillingProfile: PropTypes.object,
51+
showLabel: PropTypes.bool,
52+
sx: PropTypes.object,
53+
};
54+
55+
export default BillingProfileDropdown;
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { mount } from 'cypress/react';
2+
import { ThemeProvider } from '@mui/material/styles';
3+
import React from 'react';
4+
import globalTheme from 'modules/theme';
5+
import BillingProfileDropdown from './BillingProfileDropdown';
6+
7+
interface BillingProfile {
8+
id: string;
9+
profileName?: string;
10+
[key: string]: any;
11+
}
12+
13+
const mockBillingProfiles: BillingProfile[] = [
14+
{
15+
id: 'profile-1',
16+
profileName: 'Default Profile',
17+
},
18+
{
19+
id: 'profile-2',
20+
profileName: 'Alternative Profile',
21+
},
22+
{
23+
id: 'profile-3',
24+
profileName: 'Test Profile',
25+
},
26+
];
27+
28+
const setUp = (overrideProps = {}) => {
29+
const props = {
30+
billingProfiles: mockBillingProfiles,
31+
selectedBillingProfile: mockBillingProfiles[0],
32+
onSelectedItem: cy.stub(),
33+
...overrideProps,
34+
};
35+
36+
mount(
37+
<ThemeProvider theme={globalTheme}>
38+
<BillingProfileDropdown {...props} />
39+
</ThemeProvider>,
40+
);
41+
42+
return props;
43+
};
44+
45+
describe('BillingProfileDropdown Component', () => {
46+
// No need for beforeEach since we create fresh stubs in each test
47+
48+
describe('Rendering', () => {
49+
it('should render the billing profile label by default', () => {
50+
setUp();
51+
52+
cy.contains('Billing Profile').should('be.visible');
53+
});
54+
55+
it('should not render label when showLabel is false', () => {
56+
setUp({ showLabel: false });
57+
58+
cy.contains('Billing Profile').should('not.exist');
59+
});
60+
61+
it('should render dropdown with correct initial value', () => {
62+
setUp();
63+
64+
cy.get('[data-cy="billingProfile"]').should('contain', 'Default Profile');
65+
});
66+
});
67+
68+
describe('Profile Selection', () => {
69+
it('should display all available billing profiles', () => {
70+
setUp();
71+
72+
cy.get('[data-cy="billingProfile"]').click();
73+
cy.contains('Default Profile').should('exist');
74+
cy.contains('Alternative Profile').should('exist');
75+
cy.contains('Test Profile').should('exist');
76+
});
77+
78+
it('should call onSelectedItem when profile is selected', () => {
79+
const props = setUp();
80+
81+
cy.get('[data-cy="billingProfile"]').click();
82+
cy.contains('Alternative Profile').click();
83+
84+
cy.wrap(props.onSelectedItem).should('have.been.calledOnce');
85+
cy.wrap(props.onSelectedItem).should('have.been.calledWith', mockBillingProfiles[1]);
86+
});
87+
88+
it('should filter out profiles without profileName', () => {
89+
const profilesWithUndefined = [
90+
...mockBillingProfiles,
91+
{ id: 'profile-4' }, // Missing profileName
92+
{ id: 'profile-5', profileName: undefined },
93+
];
94+
95+
setUp({ billingProfiles: profilesWithUndefined });
96+
97+
cy.get('[data-cy="billingProfile"]').click();
98+
cy.contains('Default Profile').should('exist');
99+
cy.contains('Alternative Profile').should('exist');
100+
cy.contains('Test Profile').should('exist');
101+
// Profiles without profileName should not appear
102+
cy.get('[data-cy="menuItem-undefined"]').should('not.exist');
103+
});
104+
});
105+
106+
describe('Disabled State', () => {
107+
it('should be disabled when only one profile is available', () => {
108+
setUp({ billingProfiles: [mockBillingProfiles[0]] });
109+
110+
cy.get('[data-cy="billingProfile"]').should('have.class', 'Mui-disabled');
111+
});
112+
113+
it('should be disabled when disabled prop is true', () => {
114+
setUp({ disabled: true });
115+
116+
cy.get('[data-cy="billingProfile"]').should('have.class', 'Mui-disabled');
117+
});
118+
119+
it('should be enabled when multiple profiles available and not explicitly disabled', () => {
120+
setUp();
121+
122+
cy.get('[data-cy="billingProfile"]').should('not.have.attr', 'aria-disabled');
123+
});
124+
});
125+
126+
describe('Empty State', () => {
127+
it('should handle empty billing profiles array', () => {
128+
setUp({ billingProfiles: [], selectedBillingProfile: null });
129+
130+
cy.get('[data-cy="billingProfile"]').should('have.value', '');
131+
cy.get('[data-cy="billingProfile"]').should('have.class', 'Mui-disabled');
132+
});
133+
134+
it('should handle null selectedBillingProfile', () => {
135+
setUp({ selectedBillingProfile: null });
136+
137+
cy.get('[data-cy="billingProfile"]').should('have.value', '');
138+
});
139+
});
140+
141+
describe('Styling and Props', () => {
142+
it('should apply custom sx props', () => {
143+
const customSx = { height: '3rem', marginTop: '16px', backgroundColor: 'red' };
144+
setUp({ sx: customSx });
145+
146+
cy.get('[data-cy="billingProfile"]').should('exist');
147+
// Note: Testing exact styles is complex in Cypress, but we can verify the component renders
148+
});
149+
150+
it('should handle duplicate profile names correctly', () => {
151+
const duplicateProfiles = [
152+
{ id: 'profile-1', profileName: 'Same Name' },
153+
{ id: 'profile-2', profileName: 'Same Name' },
154+
{ id: 'profile-3', profileName: 'Different Name' },
155+
];
156+
157+
setUp({ billingProfiles: duplicateProfiles });
158+
159+
cy.get('[data-cy="billingProfile"]').click();
160+
// Should only show unique profile names
161+
cy.contains('Same Name').should('exist');
162+
cy.contains('Different Name').should('exist');
163+
});
164+
});
165+
166+
describe('Integration', () => {
167+
it('should work with real-world profile data structure', () => {
168+
it('should call onSelectedItem with the correct profile when selection changes', () => {
169+
const realWorldProfiles = [
170+
{
171+
id: 'bp-12345',
172+
profileName: 'Production Environment',
173+
billingAccountId: 'ba-prod-001',
174+
description: 'Production billing profile for live workloads',
175+
},
176+
{
177+
id: 'bp-67890',
178+
profileName: 'Development Environment',
179+
billingAccountId: 'ba-dev-001',
180+
description: 'Development billing profile for testing',
181+
},
182+
];
183+
184+
const props = setUp({
185+
billingProfiles: realWorldProfiles,
186+
selectedBillingProfile: realWorldProfiles[0],
187+
});
188+
189+
cy.get('[data-cy="billingProfile"]').should('contain', 'Production Environment');
190+
191+
cy.get('[data-cy="billingProfile"]').click();
192+
cy.contains('Development Environment').click();
193+
194+
cy.wrap(props.onSelectedItem).should('have.been.calledOnce');
195+
cy.wrap(props.onSelectedItem.getCall(0).args[0])
196+
.should('have.property', 'id', 'bp-67890')
197+
.should('have.property', 'profileName', 'Development Environment');
198+
});
199+
});
200+
});
201+
});

0 commit comments

Comments
 (0)