Skip to content

Commit

Permalink
Add marketplace ui to patternfly
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcusk19 committed Feb 1, 2024
1 parent 2ab7dc2 commit c979934
Show file tree
Hide file tree
Showing 9 changed files with 528 additions and 5 deletions.
1 change: 1 addition & 0 deletions static/js/directives/ui/usage-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ angular.module('quay').directive('usageChart', function () {
}

var finalAmount = $scope.total + $scope.marketplaceTotal;
if(finalAmount >= 9223372036854775807) { finalAmount = "unlimited"; }
chart.update($scope.current, finalAmount);
};

Expand Down
4 changes: 3 additions & 1 deletion util/marketplace.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,16 @@ def get_list_of_subscriptions(
"username": "free_user",
}

E2E_TEST_USER_EMAIL = "user1@redhat.com"


class FakeUserApi(RedHatUserApi):
"""
Fake class used for tests
"""

def lookup_customer_id(self, email):
if email == TEST_USER["email"]:
if email == TEST_USER["email"] or email == E2E_TEST_USER_EMAIL:
return TEST_USER["account_number"]
if email == FREE_USER["email"]:
return FREE_USER["account_number"]
Expand Down
58 changes: 58 additions & 0 deletions web/cypress/e2e/marketplace.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
describe('Marketplace Section', () => {
beforeEach(() => {
cy.exec('npm run quay:seed');
cy.request('GET', `${Cypress.env('REACT_QUAY_APP_API_URL')}/csrf_token`)
.then((response) => response.body.csrf_token)
.then((token) => {
cy.loginByCSRF(token);
});
});

it('ListSubscriptions', () => {
cy.visit('/organization/user1?tab=Settings');
cy.get('#pf-tab-1-billinginformation').click();
cy.get('#user-subscription-list').contains(
'2x MW02701 belonging to user namespace',
);
cy.get('#user-subscription-list').contains(
'1x MW02701 belonging to user namespace',
);
});

it('ManageSubscription', () => {
cy.visit('/organization/projectquay?tab=Settings');
cy.get('#pf-tab-1-billinginformation').click();
cy.get('#attach-subscription-button').click();
cy.get('#subscription-select-toggle').click();
cy.get('#subscription-select-list').contains('2x MW02701').click();
cy.get('#confirm-subscription-select').click();
cy.get('#org-subscription-list')
.contains('2x MW02701 attached')
.should('exist');

cy.get('#remove-subscription-button').click();
cy.get('#subscription-select-toggle').click();
cy.get('#subscription-select-list').contains('2x MW02701').click();
cy.get('#confirm-subscription-select').click();
cy.get('#org-subscription-list')
.contains('2x MW02701 attached')
.should('not.exist');
});

it('ViewUnlimitedSubscriptions', () => {
cy.intercept('GET', '/api/v1/organization/projectquay/marketplace', {
fixture: 'marketplace-org.json',
});
cy.visit('/organization/projectquay?tab=Settings');
cy.get('#pf-tab-1-billinginformation').click();
cy.get('#attach-subscription-button').click();
cy.get('#subscription-select-toggle').click();
cy.get('#subscription-select-list').contains('2x MW02701').click();
cy.get('#confirm-subscription-select').click();
cy.get('#pf-tab-0-generalsettings').click();
cy.get('#pf-tab-1-billinginformation').click();
cy.get('#form-form')
.contains('0 of unlimited private repositories used')
.should('exist');
});
});
18 changes: 18 additions & 0 deletions web/cypress/fixtures/marketplace-org.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"id": 1,
"subscription_id": 12345678,
"user_id": 36,
"org_id": 37,
"quantity": 2,
"sku": "MW02702",
"metadata": {
"title": "premium",
"privateRepos": 9007199254740991,
"stripeId": "not_a_stripe_plan",
"rh_sku": "MW02702",
"sku_billing": true,
"plans_page_hidden": true
}
}
]
133 changes: 133 additions & 0 deletions web/src/components/modals/OrgSubscriptionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
Button,
Flex,
FlexItem,
HelperText,
MenuToggle,
MenuToggleElement,
Modal,
ModalVariant,
Select,
SelectList,
SelectOption,
} from '@patternfly/react-core';
import React from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {useManageOrgSubscriptions} from 'src/hooks/UseMarketplaceSubscriptions';

interface OrgSubscriptionModalProps {
modalType: string;
org: string;
subscriptions: Array<Dict<string>>;
isOpen: boolean;
handleModalToggle: any;
}

export default function OrgSubscriptionModal(props: OrgSubscriptionModalProps) {
const [selectedSku, setSelectedSku] = React.useState('');
const [menuIsOpen, setMenuIsOpen] = React.useState(false);
const {addAlert} = useAlerts();
const onSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
value: string | number | undefined,
) => {
setSelectedSku(value as string);
setMenuIsOpen(false);
};

const {
manageSubscription,
errorManageSubscription,
successManageSubscription,
} = useManageOrgSubscriptions(props.org, {
onSuccess: () => {
addAlert({
variant: AlertVariant.Success,
title: `Successfully ${
props.modalType === 'attach' ? 'attached' : 'removed'
} subscription`,
});
},
onError: () => {
addAlert({
variant: AlertVariant.Failure,
title: `Failed to ${
props.modalType === 'attach' ? 'attach' : 'remove'
} subscription`,
});
},
});

return (
<Modal
variant={ModalVariant.small}
title={
props.modalType === 'attach'
? 'Attach Subscription'
: 'Remove Subscription'
}
isOpen={props.isOpen}
onClose={props.handleModalToggle}
>
<Flex>
<FlexItem>
<Select
id="subscription-single-select"
isOpen={menuIsOpen}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
id="subscription-select-toggle"
ref={toggleRef}
onClick={() => setMenuIsOpen(!menuIsOpen)}
isExpanded={menuIsOpen}
>
{props.subscriptions[selectedSku]
? props.subscriptions[selectedSku]?.quantity +
'x ' +
props.subscriptions[selectedSku]?.sku
: 'Select Subscription'}
</MenuToggle>
)}
selected={selectedSku}
onSelect={onSelect}
onOpenChange={(isOpen) => setMenuIsOpen(isOpen)}
shouldFocusToggleOnSelect
>
<SelectList id="subscription-select-list">
{props.subscriptions.map((subscription: Dict<string>, index) => (
<SelectOption key={subscription.id} value={index}>
{subscription.quantity}x {subscription.sku}
</SelectOption>
))}
</SelectList>
</Select>
</FlexItem>
<FlexItem>
<Button
id="confirm-subscription-select"
variant="primary"
onClick={() => {
props.handleModalToggle();
manageSubscription({
subscription: props.subscriptions[selectedSku],
manageType: props.modalType,
});
}}
>
Confirm
</Button>
</FlexItem>
<FlexItem>
<Button
id="cancel-subscription-select"
variant="secondary"
onClick={props.handleModalToggle}
>
Cancel
</Button>
</FlexItem>
</Flex>
</Modal>
);
}
93 changes: 93 additions & 0 deletions web/src/hooks/UseMarketplaceSubscriptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {
fetchMarketplaceSubscriptions,
setMarketplaceOrgAttachment,
setMarketplaceOrgRemoval,
} from 'src/resources/BillingResource';
import {useCurrentUser} from './UseCurrentUser';
import {useQuayConfig} from './UseQuayConfig';

export function useMarketplaceSubscriptions(
organizationName: string = null,
userName: string = null,
) {
const config = useQuayConfig();
const {
isLoading: loadingUserSubs,
isError: errorFetchingUserSubs,
data: userSubscriptions,
} = useQuery(
['subscriptions', {type: 'user'}],
() => fetchMarketplaceSubscriptions(),
{
enabled: config?.features?.RH_MARKETPLACE,
},
);

const {
isLoading: loadingOrgSubs,
error: errorFetchingOrgSubs,
data: orgSubscriptions,
} = useQuery(
['subscriptions', {type: 'org'}],
() => fetchMarketplaceSubscriptions(organizationName),
{
enabled: config?.features?.RH_MARKETPLACE && organizationName != userName,
},
);

const loading =
organizationName != userName
? loadingUserSubs || loadingOrgSubs
: loadingUserSubs;

return {
userSubscriptions: userSubscriptions,
orgSubscriptions: orgSubscriptions,
loading: loading,
error: errorFetchingUserSubs,
};
}

export function useManageOrgSubscriptions(org: string, {onSuccess, onError}) {
const queryClient = useQueryClient();

const {
mutate: manageSubscription,
isError: errorManageSubscription,
isSuccess: successManageSubscription,
} = useMutation(
async ({
subscription,
manageType,
}: {
subscription: Dict<string>;
manageType: string;
}) => {
const reqBody = [];
reqBody.push({
subscription_id:
manageType === 'attach'
? subscription['id']
: subscription['subscription_id'],
});
if (manageType === 'attach') {
setMarketplaceOrgAttachment(org, reqBody);
} else {
setMarketplaceOrgRemoval(org, reqBody);
}
},
{
onSuccess: () => {
onSuccess();
return queryClient.invalidateQueries(['subscriptions']);
},
onError: onError,
},
);
return {
manageSubscription,
errorManageSubscription,
successManageSubscription,
};
}
51 changes: 48 additions & 3 deletions web/src/resources/BillingResource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {AxiosResponse} from 'axios';
import axios from 'src/libs/axios';
import {ResourceError} from './ErrorHandling';

export interface Subscription {
hasSubscription: boolean;
Expand Down Expand Up @@ -47,9 +48,8 @@ export interface Plan {
}

export async function fetchPlans() {
const response: AxiosResponse<PlansResponse> = await axios.get(
'/api/v1/plans/',
);
const response: AxiosResponse<PlansResponse> =
await axios.get('/api/v1/plans/');
return response.data.plans;
}

Expand Down Expand Up @@ -85,3 +85,48 @@ export async function fetchCard(org: string = null) {
const response: AxiosResponse<BillingCardResponse> = await axios.get(url);
return response.data.card;
}

export async function fetchMarketplaceSubscriptions(org: string = null) {
const url: string =
org != null
? `/api/v1/organization/${org}/marketplace`
: '/api/v1/user/marketplace';
const response: AxiosResponse = await axios.get(url);
return response.data;
}

export async function setMarketplaceOrgAttachment(
org: string,
subscriptions: Array<any>,
) {
try {
await axios.post(`/api/v1/organization/${org}/marketplace`, {
subscriptions: subscriptions,
});
} catch (error) {
throw new ResourceError(
'Unable to bind subscriptions to org',
`${org}`,
error,
);
}
return;
}

export async function setMarketplaceOrgRemoval(
org: string,
subscriptions: Array<any>,
) {
try {
await axios.post(`/api/v1/organization/${org}/marketplace/batchremove`, {
subscriptions: subscriptions,
});
} catch (error) {
throw new ResourceError(
'Unable to remove subscriptions from org',
`${org}`,
error,
);
}
return;
}
Loading

0 comments on commit c979934

Please sign in to comment.