Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DT-1001] select billing profile #1735

Merged
merged 6 commits into from
Jan 15, 2025
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
2 changes: 1 addition & 1 deletion src/actions/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const { getBillingProfileById } = createActions({
});

export const { createSnapshot } = createActions({
[ActionTypes.CREATE_SNAPSHOT]: () => ({}),
[ActionTypes.CREATE_SNAPSHOT]: (billingProfileId) => ({ billingProfileId }),
[ActionTypes.CREATE_SNAPSHOT_JOB]: (snapshot) => snapshot,
[ActionTypes.CREATE_SNAPSHOT_SUCCESS]: (snapshot) => snapshot,
[ActionTypes.CREATE_SNAPSHOT_FAILURE]: (snapshot) => snapshot,
Expand Down
67 changes: 42 additions & 25 deletions src/components/common/FullViewSnapshotButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { ThemeProvider } from '@mui/styles';
import { Provider } from 'react-redux';
import React from 'react';
import createMockStore from 'redux-mock-store';
import { BillingProfileModel, DatasetModel } from 'generated/tdr';
import _ from 'lodash';
import { initialUserState } from 'reducers/user';
import { initialQueryState } from 'reducers/query';
import history from '../../modules/hist';
import globalTheme from '../../modules/theme';
import FullViewSnapshotButton from './FullViewSnapshotButton';
Expand All @@ -15,14 +19,17 @@ const initialState = {
name: 'Test Snapshot',
},
},
profiles: {
profiles: [{ id: 'profile1', name: 'Test Profile 1' }],
},
user: _.cloneDeep(initialUserState),
query: _.cloneDeep(initialQueryState),
router: { location: {} },
};

const mountFullViewSnapshotButton = (dataset) => {
const mountFullViewSnapshotButton = (
dataset: DatasetModel,
billingProfiles: Array<BillingProfileModel>,
) => {
const mockStore = createMockStore([]);
const store = mockStore(initialState);
const store = mockStore({ ...initialState, profiles: { profiles: billingProfiles } });

// Intercept the getBillingProfiles API call onMount
cy.intercept('GET', '/api/resources/v1/profiles?offset=0&limit=1000').as('getBillingProfiles');
Expand All @@ -42,44 +49,54 @@ describe('FullViewSnapshotButton', () => {
describe('FullViewSnapshotButton component with permission', () => {
beforeEach(() => {
const dataset = { defaultProfileId: 'profile1' };
mountFullViewSnapshotButton(dataset);
const profiles = [
{ id: 'profile1', profileName: 'profile1' },
{ id: 'profile2', profileName: 'profile2' },
];
mountFullViewSnapshotButton(dataset, profiles);
});

it('Displays the button with correct text', () => {
cy.get('button').should('contain.text', 'Create Full View Snapshot');
});

it('Button is clickable and calls createSnapshot', () => {
it('Button is clickable and opens billing profile modal', () => {
cy.get('button').click();
cy.contains('Creating snapshot - select a billing project').should('be.visible');
cy.get('[data-cy=select-billing-profile-button]').click();
cy.intercept('POST', '/api/repository/v1/snapshots');
});
});

describe('FullViewSnapshotButton component without permission', () => {
beforeEach(() => {
const dataset = { defaultProfileId: 'profile2' };
mountFullViewSnapshotButton(dataset);
it('calls create snapshot when billing profile is selected', () => {
cy.get('button').click();
cy.get('[data-cy=select-billing-profile-button]').click();
cy.intercept('POST', '/api/repository/v1/snapshots');
});

it('Button is disabled and has tooltip with the no access message', () => {
cy.get('button').should('be.disabled');
cy.get('button').trigger('mouseover', { force: true });
cy.contains(
'You do not have access to the billing profile associated with this dataset.',
).should('be.visible');
it('allows selecting a billing profile', () => {
cy.get('button').click();
cy.get('#billing-profile-select').parent().click();
cy.get('[data-cy=menuItem-profile2]').click();
cy.get('#billing-profile-select').should('have.value', 'profile2');
});
});

describe('FullViewSnapshotButton component without default billing profile', () => {
beforeEach(() => {
const dataset = { defaultProfileId: null };
mountFullViewSnapshotButton(dataset);
it('allows changing the name and description', () => {
cy.get('button').click();
cy.get('#snapshot-name').clear().type('New Name');
cy.get('#snapshot-description').clear().type('New Description');
cy.get('#snapshot-name').should('have.value', 'New Name');
cy.get('#snapshot-description').should('have.value', 'New Description');
});
});

it('Button is disabled and has tooltip with the no billing profile message', () => {
describe('FullViewSnapshotButton component without permission', () => {
it('Button is disabled and has tooltip with the no access message', () => {
const dataset = { defaultProfileId: 'profile2' };
const profiles: BillingProfileModel[] = [];
mountFullViewSnapshotButton(dataset, profiles);
cy.get('button').should('be.disabled');
cy.get('button').trigger('mouseover', { force: true });
cy.contains('There is no default billing profile associated with this dataset.').should(
cy.contains('You do not have access to any billing profiles to create a snapshot').should(
'be.visible',
);
});
Expand Down
164 changes: 137 additions & 27 deletions src/components/common/FullViewSnapshotButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { createSnapshot, getBillingProfiles, snapshotCreateDetails } from 'actions/index';
import { TdrState } from 'reducers';
import TerraTooltip from 'components/common/TerraTooltip';
import { Button } from '@mui/material';
import {
Button,
Dialog,
DialogActions,
DialogTitle,
Typography,
FormLabel,
TextField,
} from '@mui/material';
import React, { Dispatch } from 'react';
import {
BillingProfileModel,
Expand All @@ -10,7 +17,9 @@ import {
} from 'generated/tdr';
import { Action } from 'redux';
import { connect } from 'react-redux';
import { now } from 'lodash';
import { isEmpty, now, uniq } from 'lodash';
import TerraTooltip from './TerraTooltip';
import JadeDropdown from '../dataset/data/JadeDropdown';
import { useOnMount } from '../../libs/utils';

interface FullViewSnapshotButtonProps {
Expand All @@ -28,44 +37,145 @@ function FullViewSnapshotButton({
dispatch(getBillingProfiles());
});

const defaultBillingProfile = dataset.defaultProfileId;
const defaultBillingProfile = billingProfiles.find(
(billingProfile) => billingProfile.id === dataset.defaultProfileId,
);

const [modalOpen, setModalOpen] = React.useState(false);
const [selectedBillingProfile, setSelectedBillingProfile] = React.useState(defaultBillingProfile);
const [snapshotName, setSnapshotName] = React.useState(
`Full_View_Snapshot_of_${dataset.name}_${now()}`,
);
const [snapshotDescription, setSnapshotDescription] = React.useState(
`Full View Snapshot of Dataset with Dataset name ${dataset.name}, and Dataset id ${dataset.id}.`,
);

// if the default billing profile is undefined or the user does not have permission on it, disable button and show tooltip
const hasAccess = billingProfiles.some((model) => model.id === defaultBillingProfile);
const isDisabled = billingProfiles.length === 0;
let tooltipText = '';
if (!defaultBillingProfile) {
tooltipText = 'There is no default billing profile associated with this dataset.';
} else if (!hasAccess) {
tooltipText = 'You do not have access to the billing profile associated with this dataset.';
if (isDisabled) {
tooltipText = 'You do not have access to any billing profiles to create a snapshot';
}
const isDisabled = !defaultBillingProfile || !hasAccess;

const handleCreateFullViewSnapshot = () => {
const name = `Full_View_Snapshot_of_${dataset.name}_${now()}`;
const description = `Full View Snapshot of Dataset with Dataset name ${dataset.name}, and Dataset id ${dataset.id}.`;
dispatch(
snapshotCreateDetails(
name,
description,
snapshotName,
snapshotDescription,
SnapshotRequestContentsModelModeEnum.ByFullView,
dataset,
),
);
dispatch(createSnapshot());
dispatch(createSnapshot(selectedBillingProfile?.id));
};

const onDismiss = () => setModalOpen(false);

const onSelect = () => {
handleCreateFullViewSnapshot();
setModalOpen(false);
};

return (
<TerraTooltip title={isDisabled ? tooltipText : ''}>
<span>
<Button
variant="outlined"
disableElevation
onClick={() => handleCreateFullViewSnapshot()}
disabled={isDisabled}
>
Create Full View Snapshot
</Button>
</span>
</TerraTooltip>
<>
<TerraTooltip title={isDisabled ? tooltipText : ''}>
<span>
<Button
variant="outlined"
disableElevation
onClick={() => {
setSelectedBillingProfile(defaultBillingProfile || billingProfiles[0]);
setModalOpen(true);
}}
disabled={isDisabled}
>
Create Full View Snapshot
</Button>
</span>
</TerraTooltip>
<Dialog fullWidth maxWidth="sm" onClose={onDismiss} open={modalOpen}>
<DialogTitle id="customized-dialog-title" sx={{ fontSize: '1rem' }}>
Creating snapshot - select a billing project
</DialogTitle>
<div style={{ padding: '0px 24px 16px 24px' }}>
<FormLabel sx={{ fontWeight: 600, color: 'black' }} htmlFor="snapshot-name" required>
Snapshot Name
</FormLabel>
<TextField
fullWidth
margin="normal"
id="snapshot-name"
label="Snapshot Name"
value={snapshotName}
onChange={(e) => setSnapshotName(e.target.value)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to do input validation here for name and description because we are sending it to the backend?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backend has validation - I checked other places and TDR UI doesn't currently validate inputs - it just pushes the problem to the API. We probably want clientside validation at some point, but that should be out of scope of this. (That's also when I would add more testing)

/>
<FormLabel
sx={{ fontWeight: 600, color: 'black' }}
htmlFor="snapshot-description"
required
>
Snapshot Description
</FormLabel>
<TextField
fullWidth
margin="normal"
id="snapshot-description"
label="Snapshot Description"
value={snapshotDescription}
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?
</Typography>
<div style={{ marginTop: 8 }}>
<FormLabel
sx={{ fontWeight: 600, color: 'black' }}
htmlFor="billing-profile-select"
required
>
Google Billing Project
</FormLabel>
</div>
<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 || ''}
/>
<Typography>If this is the correct billing project - just click select</Typography>
<DialogActions sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={onDismiss} variant="outlined">
Cancel
</Button>
<Button
onClick={onSelect}
disabled={
selectedBillingProfile?.id === undefined ||
isEmpty(snapshotName) ||
isEmpty(snapshotDescription)
}
variant="contained"
data-cy="select-billing-profile-button"
>
Create
</Button>
</DialogActions>
</div>
</Dialog>
</>
);
}

Expand Down
4 changes: 3 additions & 1 deletion src/components/dataset/data/JadeDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ type IProps<T> = {
onSelectedItem: (e: SelectChangeEvent) => void;
options: T[];
value: T;
sx?: React.CSSProperties;
};

function JadeDropdown({ disabled, name, onSelectedItem, options, value }: IProps<string>) {
function JadeDropdown({ disabled, name, onSelectedItem, options, value, sx }: IProps<string>) {
return (
<form autoComplete="off">
<FormControl disabled={disabled} variant="outlined" fullWidth>
Expand All @@ -24,6 +25,7 @@ function JadeDropdown({ disabled, name, onSelectedItem, options, value }: IProps
displayEmpty
renderValue={(val) => (!val ? name : val)}
data-cy={_.camelCase(name)}
sx={sx}
>
{options.map((opt) => (
<MenuItem key={opt} value={opt} data-cy={`menuItem-${opt}`}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class ShareSnapshot extends React.PureComponent {

saveSnapshot = () => {
const { dispatch } = this.props;
dispatch(createSnapshot());
dispatch(createSnapshot(undefined));
};

render() {
Expand Down
5 changes: 3 additions & 2 deletions src/sagas/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ export function* resetSnapshotExport() {
* Snapshots.
*/

export function* createSnapshot(): any {
export function* createSnapshot({ payload }: any): any {
const { billingProfileId } = payload;
const snapshots = yield select(getSnapshotState);
const dataset = yield select(getDataset);
const {
Expand All @@ -238,7 +239,7 @@ export function* createSnapshot(): any {
const datasetName = dataset.name;
const snapshotRequest = {
name,
profileId: dataset.defaultProfileId,
profileId: billingProfileId || dataset.defaultProfileId,
description,
policies,
contents: [],
Expand Down
Loading