diff --git a/.changeset/open-actors-own.md b/.changeset/open-actors-own.md new file mode 100644 index 00000000000..a1965bc7b8f --- /dev/null +++ b/.changeset/open-actors-own.md @@ -0,0 +1,5 @@ +--- +'@finos/legend-application-marketplace': patch +--- + +terminals and addons wokflow integration with view orders page diff --git a/.changeset/ripe-views-push.md b/.changeset/ripe-views-push.md new file mode 100644 index 00000000000..d443efc8e7d --- /dev/null +++ b/.changeset/ripe-views-push.md @@ -0,0 +1,5 @@ +--- +'@finos/legend-art': patch +--- + +added style variables for terminals and addons diff --git a/.changeset/swift-emus-go.md b/.changeset/swift-emus-go.md new file mode 100644 index 00000000000..69bc2b4acb4 --- /dev/null +++ b/.changeset/swift-emus-go.md @@ -0,0 +1,5 @@ +--- +'@finos/legend-server-marketplace': patch +--- + +added cart and order stores for terminals and addons diff --git a/packages/legend-application-marketplace/src/__lib__/LegendMarketplaceAppEvent.ts b/packages/legend-application-marketplace/src/__lib__/LegendMarketplaceAppEvent.ts index 8c11f30f201..fca6ec69e9d 100644 --- a/packages/legend-application-marketplace/src/__lib__/LegendMarketplaceAppEvent.ts +++ b/packages/legend-application-marketplace/src/__lib__/LegendMarketplaceAppEvent.ts @@ -30,4 +30,5 @@ export enum LEGEND_MARKETPLACE_APP_EVENT { CLICK_SUBSCRIBE_TO_NEWSLETTER = 'marketplace.click.subscribe.to.newsletter', OPEN_INTEGRATED_PRODUCT = 'marketplace.open.integrated.product', FETCH_PENDING_CONTRACT = 'marketplace.fetch.pending-contract.failure', + ORDER_CANCELLATION_FAILURE = 'marketplace.order.cancellation.failure', } diff --git a/packages/legend-application-marketplace/src/components/AddToCart/RecommendedAddOnsModal.tsx b/packages/legend-application-marketplace/src/components/AddToCart/RecommendedAddOnsModal.tsx index 5b27bf96ca0..b5a690660ca 100644 --- a/packages/legend-application-marketplace/src/components/AddToCart/RecommendedAddOnsModal.tsx +++ b/packages/legend-application-marketplace/src/components/AddToCart/RecommendedAddOnsModal.tsx @@ -15,6 +15,7 @@ */ import { observer } from 'mobx-react-lite'; +import { useMemo, useState } from 'react'; import { Dialog, DialogTitle, @@ -22,15 +23,24 @@ import { DialogActions, Button, Typography, - Grid, Box, IconButton, + TextField, + InputAdornment, + Select, + MenuItem, + FormControl, + InputLabel, + type SelectChangeEvent, } from '@mui/material'; import { CloseIcon, CheckCircleIcon, ArrowRightIcon, WarningIcon, + SearchIcon, + ArrowUpIcon, + ArrowDownIcon, } from '@finos/legend-art'; import { TerminalItemType, @@ -58,8 +68,34 @@ export const RecommendedAddOnsModal = observer( onViewCart, } = props; + const [searchTerm, setSearchTerm] = useState(''); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | ''>(''); + + const filteredAndSortedItems = useMemo(() => { + let items = [...recommendedItems]; + + if (searchTerm) { + const search = searchTerm.toLowerCase(); + items = items.filter( + (item) => + item.productName.toLowerCase().includes(search) || + item.providerName.toLowerCase().includes(search), + ); + } + + if (sortOrder) { + items.sort((a, b) => + sortOrder === 'asc' ? a.price - b.price : b.price - a.price, + ); + } + + return items; + }, [recommendedItems, searchTerm, sortOrder]); + const closeModal = () => { setShowModal(false); + setSearchTerm(''); + setSortOrder(''); }; const handleViewCart = () => { @@ -67,6 +103,12 @@ export const RecommendedAddOnsModal = observer( closeModal(); }; + const handleSortChange = ( + event: SelectChangeEvent<'asc' | 'desc' | ''>, + ) => { + setSortOrder(event.target.value); + }; + if (!showModal) { return null; } @@ -119,8 +161,10 @@ export const RecommendedAddOnsModal = observer( className="recommended-addons-modal__section-title" > {terminal?.terminalItemType === TerminalItemType.TERMINAL - ? 'Recommended Add-Ons' - : 'Recommended Terminals'} + ? `Recommended Add-Ons for ${terminal.providerName}` + : terminal + ? `Recommended Terminals for ${terminal.providerName}` + : ''} ) : ( - - {recommendedItems.map((item) => ( - - {terminal ? ( - - ) : ( + <> + + setSearchTerm(e.target.value)} + className="recommended-addons-modal__search-field" + slotProps={{ + input: { + endAdornment: ( + + + + ), + }, + }} + /> + + + Sort by Price + + + + + + {filteredAndSortedItems.length === 0 ? ( + + + No items match your search criteria. + + + ) : ( + + + + Product Name + + + Provider + + + Price + + + Action + + + {filteredAndSortedItems.map((item) => ( - )} - - ))} - + ))} + + )} + )} diff --git a/packages/legend-application-marketplace/src/components/AddToCart/RecommendedItemsCard.tsx b/packages/legend-application-marketplace/src/components/AddToCart/RecommendedItemsCard.tsx index bd3a2ffadc8..632519d34ba 100644 --- a/packages/legend-application-marketplace/src/components/AddToCart/RecommendedItemsCard.tsx +++ b/packages/legend-application-marketplace/src/components/AddToCart/RecommendedItemsCard.tsx @@ -16,16 +16,7 @@ import { clsx, PlusIcon } from '@finos/legend-art'; import type { TerminalResult } from '@finos/legend-server-marketplace'; -import { - Box, - Button, - Card, - CardActions, - CardContent, - Chip, - CircularProgress, - Typography, -} from '@mui/material'; +import { Box, Button, CircularProgress, Typography } from '@mui/material'; import { flowResult } from 'mobx'; import { useState } from 'react'; import { assertErrorThrown } from '@finos/legend-shared'; @@ -33,12 +24,11 @@ import { toastManager } from '../Toast/CartToast.js'; import { useLegendMarketplaceBaseStore } from '../../application/providers/LegendMarketplaceFrameworkProvider.js'; interface RecommendedItemsCardProps { - vendorProfileId?: number; recommendedItem: TerminalResult; } export const RecommendedItemsCard = (props: RecommendedItemsCardProps) => { - const { vendorProfileId, recommendedItem } = props; + const { recommendedItem } = props; const legendMarketplaceBaseStore = useLegendMarketplaceBaseStore(); const [isAddingToCart, setIsAddingToCart] = useState(false); const [inCart, setInCart] = useState(false); @@ -47,9 +37,6 @@ export const RecommendedItemsCard = (props: RecommendedItemsCardProps) => { setIsAddingToCart(true); const cartItemRequest = legendMarketplaceBaseStore.cartStore.providerToCartRequest(addon); - if (vendorProfileId) { - cartItemRequest.vendorProfileId = vendorProfileId; - } flowResult( legendMarketplaceBaseStore.cartStore.addToCartWithAPI(cartItemRequest), @@ -70,62 +57,45 @@ export const RecommendedItemsCard = (props: RecommendedItemsCardProps) => { }; return ( - - - - - {recommendedItem.productName} - - - - - - {recommendedItem.description || 'No description available'} - - - - + + {recommendedItem.productName} + + + {recommendedItem.providerName} + + + {recommendedItem.price === 0 + ? 'Free' + : recommendedItem.price.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 2, })} - > - {recommendedItem.price === 0 - ? 'Free' - : recommendedItem.price.toLocaleString('en-US', { - style: 'currency', - currency: 'USD', - maximumFractionDigits: 2, - })} - - - - - + + - - + + ); }; diff --git a/packages/legend-application-marketplace/src/components/ProviderCard/LegendMarketplaceTerminalCard.tsx b/packages/legend-application-marketplace/src/components/ProviderCard/LegendMarketplaceTerminalCard.tsx index 8e4d0e9bca2..3417eaddedf 100644 --- a/packages/legend-application-marketplace/src/components/ProviderCard/LegendMarketplaceTerminalCard.tsx +++ b/packages/legend-application-marketplace/src/components/ProviderCard/LegendMarketplaceTerminalCard.tsx @@ -25,6 +25,7 @@ import { CardMedia, Chip, CircularProgress, + Typography, } from '@mui/material'; import type { TerminalResult } from '@finos/legend-server-marketplace'; import { CheckCircleIcon, ShoppingCartIcon } from '@finos/legend-art'; @@ -104,10 +105,25 @@ export const LegendMarketplaceTerminalCard = observer( image={getImageUrl()} alt="data asset" /> + {terminalResult.category && ( + + )} - + + {terminalResult.providerName} + + {terminalResult.productName} - + diff --git a/packages/legend-application-marketplace/src/components/orders/CancelOrderDialog.tsx b/packages/legend-application-marketplace/src/components/orders/CancelOrderDialog.tsx new file mode 100644 index 00000000000..61909f19ef3 --- /dev/null +++ b/packages/legend-application-marketplace/src/components/orders/CancelOrderDialog.tsx @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2025-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState } from 'react'; +import { observer } from 'mobx-react-lite'; +import { flowResult } from 'mobx'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Typography, + Box, + CircularProgress, +} from '@mui/material'; +import { WarningIcon } from '@finos/legend-art'; +import type { TerminalProductOrder } from '@finos/legend-server-marketplace'; +import type { OrdersStore } from '../../stores/orders/OrderStore.js'; +import { getProcessInstanceId } from '../../stores/orders/OrderHelpers.js'; + +interface CancelOrderDialogProps { + open: boolean; + onClose: () => void; + order: TerminalProductOrder; + orderStore: OrdersStore; +} + +export const CancelOrderDialog: React.FC = observer( + ({ open, onClose, order, orderStore }) => { + const [cancellationReason, setCancellationReason] = useState(''); + + const isLoading = orderStore.cancelOrderState.isInProgress; + const trimmedReason = cancellationReason.trim(); + const isReasonValid = trimmedReason.length > 0; + + const handleClose = (): void => { + if (!isLoading) { + setCancellationReason(''); + onClose(); + } + }; + + const handleConfirm = async (): Promise => { + if (!isReasonValid) { + return; + } + + const processInstanceId = getProcessInstanceId(order); + if (!processInstanceId) { + return; + } + + const success = await flowResult( + orderStore.cancelOrder( + order.order_id, + processInstanceId, + trimmedReason, + ), + ); + if (success) { + handleClose(); + } + }; + + return ( + + + + + Cancel Order + + + + + + + Are you sure you want to cancel this order? + + + + + Order ID: {order.order_id} + + + Vendor: {order.vendor_name} + + + Order Type: {order.order_type} + + + + setCancellationReason(e.target.value)} + disabled={isLoading} + required={true} + helperText="Required: Please explain why you are canceling this order" + error={cancellationReason.length > 0 && !isReasonValid} + className="legend-marketplace-cancel-order-dialog__text-field" + /> + + + + + + + + + ); + }, +); diff --git a/packages/legend-application-marketplace/src/components/orders/ProgressTracker.tsx b/packages/legend-application-marketplace/src/components/orders/ProgressTracker.tsx new file mode 100644 index 00000000000..d6a44005027 --- /dev/null +++ b/packages/legend-application-marketplace/src/components/orders/ProgressTracker.tsx @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2025-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { observer } from 'mobx-react-lite'; +import { + Box, + Typography, + Stepper, + Step, + StepLabel, + StepConnector, + stepConnectorClasses, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { + CheckCircleIcon, + CircleIcon, + TimesCircleIcon, +} from '@finos/legend-art'; +import type { TerminalProductOrder } from '@finos/legend-server-marketplace'; +import { + getWorkflowSteps, + STAGE_MAP, + isStageCompleted, + isStageRejected, + formatTimestamp, + WorkflowStage, + WorkflowStatus, + WorkflowCurrentStage, +} from '../../stores/orders/OrderHelpers.js'; + +interface ProgressTrackerProps { + order: TerminalProductOrder; +} + +const CustomConnector = styled(StepConnector)(({ theme }) => ({ + [`&.${stepConnectorClasses.alternativeLabel}`]: { + top: 10, + left: 'calc(-50% + 16px)', + right: 'calc(50% + 16px)', + }, + [`&.${stepConnectorClasses.active}`]: { + [`& .${stepConnectorClasses.line}`]: { + borderColor: theme.palette.primary.main, + }, + }, + [`&.${stepConnectorClasses.completed}`]: { + [`& .${stepConnectorClasses.line}`]: { + borderColor: theme.palette.primary.main, + }, + }, + [`& .${stepConnectorClasses.line}`]: { + borderColor: theme.palette.grey[400], + borderTopWidth: 2, + borderRadius: 1, + }, +})); + +const StepIconComponent = (props: { + active: boolean; + completed: boolean; + rejected?: boolean; +}): React.ReactElement => { + const { active, completed, rejected } = props; + + if (rejected) { + return ( + + ); + } + if (completed) { + return ( + + ); + } + if (active) { + return ( + + ); + } + return ( + + ); +}; + +export const ProgressTracker: React.FC = observer( + ({ order }) => { + const steps = getWorkflowSteps(order); + const currentStageName = order.workflow_details?.current_stage + ? (STAGE_MAP[order.workflow_details.current_stage] ?? + WorkflowStage.ORDER_PLACED) + : WorkflowStage.ORDER_PLACED; + + const currentStageIndex = steps.indexOf(currentStageName); + const activeStepIndex = currentStageIndex >= 0 ? currentStageIndex : 0; + + const getFinalStepIndex = (): number => { + if (!order.workflow_details) { + return 0; + } + + for (let i = steps.length - 1; i >= 0; i--) { + if ( + isStageRejected(order, steps[i] ?? WorkflowStage.ORDER_PLACED) && + isStageCompleted(order, steps[i] ?? WorkflowStage.ORDER_PLACED) + ) { + return i; + } + } + + for (let i = steps.length - 1; i >= 0; i--) { + if (isStageCompleted(order, steps[i] ?? WorkflowStage.ORDER_PLACED)) { + return i; + } + } + + if ( + order.workflow_details.rpm_ticket_id && + order.workflow_details.current_stage === WorkflowCurrentStage.RPM + ) { + return steps.indexOf(WorkflowStage.PENDING_FULFILLMENT); + } + + return activeStepIndex; + }; + + const isClosedOrder = + order.workflow_details?.workflow_status.toString() === + WorkflowStatus.COMPLETED; + const finalStepIndex = isClosedOrder + ? getFinalStepIndex() + : activeStepIndex; + + return ( + + } + > + {steps.map((label, index) => { + const isCompleted = isClosedOrder + ? index <= finalStepIndex + : index < activeStepIndex; + const isActive = !isClosedOrder && index === activeStepIndex; + const stageCompleted = isStageCompleted(order, label); + const rejected = isStageRejected(order, label); + + return ( + + + StepIconComponent({ + active: isActive, + completed: isCompleted && !rejected, + rejected: rejected && isCompleted, + }) + } + > + + {label} + + + {label === WorkflowStage.MANAGER_APPROVAL && + isCompleted && + stageCompleted && + order.workflow_details && + !isActive && ( + + {order.workflow_details.manager_actioned_by && ( + + Actioned by:{' '} + {order.workflow_details.manager_actioned_by} + + )} + {order.workflow_details.manager_actioned_timestamp && ( + + Date:{' '} + {formatTimestamp( + order.workflow_details.manager_actioned_timestamp, + )} + + )} + {order.workflow_details.manager_action && ( + + Action:{' '} + {order.workflow_details.manager_action} + + )} + {order.workflow_details.manager_comment && ( + + Comments:{' '} + {order.workflow_details.manager_comment} + + )} + + )} + + {label === WorkflowStage.BUSINESS_ANALYST_APPROVAL && + isCompleted && + stageCompleted && + order.workflow_details && + !isActive && ( + + {order.workflow_details.bbg_approval_actioned_by && ( + + Actioned by:{' '} + {order.workflow_details.bbg_approval_actioned_by} + + )} + {order.workflow_details + .bbg_approval_actioned_timestamp && ( + + Date:{' '} + {formatTimestamp( + order.workflow_details + .bbg_approval_actioned_timestamp, + )} + + )} + {order.workflow_details.bbg_approval_action && ( + + Action:{' '} + {order.workflow_details.bbg_approval_action} + + )} + {order.workflow_details.bbg_approval_comment && ( + + Comments:{' '} + {order.workflow_details.bbg_approval_comment} + + )} + + )} + + {label === WorkflowStage.PENDING_FULFILLMENT && + order.workflow_details?.rpm_ticket_id && ( + + + RPM Ticket:{' '} + {order.workflow_details.rpm_ticket_id} + + + )} + + + ); + })} + + + ); + }, +); diff --git a/packages/legend-application-marketplace/src/index.ts b/packages/legend-application-marketplace/src/index.ts index de51e72ba1f..19a3b54d05f 100644 --- a/packages/legend-application-marketplace/src/index.ts +++ b/packages/legend-application-marketplace/src/index.ts @@ -30,3 +30,4 @@ export * from './application/LegendMarketplaceApplicationPlugin.js'; export { LegendMarketplaceProductViewerStore } from './stores/lakehouse/LegendMarketplaceProductViewerStore.js'; export { LegendMarketplaceSearchResultsStore } from './stores/lakehouse/LegendMarketplaceSearchResultsStore.js'; export { ProductCardState } from './stores/lakehouse/dataProducts/ProductCardState.js'; +export type { FetchProductsParams } from './stores/LegendMarketPlaceVendorDataStore.js'; diff --git a/packages/legend-application-marketplace/src/pages/Profile/LegendMarketplaceYourOrders.tsx b/packages/legend-application-marketplace/src/pages/Profile/LegendMarketplaceYourOrders.tsx index acd8c9fca0f..26b0928a5cb 100644 --- a/packages/legend-application-marketplace/src/pages/Profile/LegendMarketplaceYourOrders.tsx +++ b/packages/legend-application-marketplace/src/pages/Profile/LegendMarketplaceYourOrders.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { observer } from 'mobx-react-lite'; import { Box, @@ -24,218 +24,229 @@ import { Chip, CircularProgress, Button, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, - IconButton, - Collapse, + Accordion, + AccordionSummary, + AccordionDetails, + Stack, + Tooltip, } from '@mui/material'; import { flowResult } from 'mobx'; import { - RefreshIcon, ShoppingCartIcon, - ClockIcon, - CheckCircleIcon, ChevronDownIcon, - ChevronRightIcon, + TimesCircleIcon, } from '@finos/legend-art'; import { LegendMarketplacePage } from '../LegendMarketplacePage.js'; import { useLegendMarketplaceBaseStore } from '../../application/providers/LegendMarketplaceFrameworkProvider.js'; import { type TerminalProductOrder, - OrderCategory, OrderStatus, } from '@finos/legend-server-marketplace'; -import { assertErrorThrown } from '@finos/legend-shared'; +import { assertErrorThrown, isNullable } from '@finos/legend-shared'; import { useLegendMarketplaceOrdersStore, withLegendMarketplaceOrdersStore, } from '../../application/providers/LegendMarketplaceYourOrdersStoreProvider.js'; +import { ProgressTracker } from '../../components/orders/ProgressTracker.js'; +import { CancelOrderDialog } from '../../components/orders/CancelOrderDialog.js'; +import { + formatOrderDate, + canCancelOrder, +} from '../../stores/orders/OrderHelpers.js'; -const OrderStatusChip: React.FC<{ status: OrderStatus | undefined }> = ({ - status, -}) => { - switch (status) { - case OrderStatus.IN_PROGRESS: - return ( - } - label={status.toString()} - variant="filled" - size="small" - className={'order-status-chip--open'} - /> - ); - case OrderStatus.OPEN: - return ( - } - label={status.toString()} - variant="filled" - size="small" - className={'order-status-chip--open'} - /> - ); - case OrderStatus.COMPLETED: - return ( - } - label={status.toString()} - variant="filled" - size="small" - className={'order-status-chip--closed'} - /> - ); - default: - return ( - } - label={OrderStatus.IN_PROGRESS.toString()} - variant="filled" - size="small" - className={'order-status-chip--open'} - /> - ); - } -}; +const OrderAccordion: React.FC<{ + order: TerminalProductOrder; + isOpenOrder: boolean; +}> = observer(({ order, isOpenOrder }) => { + const [cancelDialogOpen, setCancelDialogOpen] = useState(false); + const ordersStore = useLegendMarketplaceOrdersStore(); -const StageChip: React.FC<{ stage: string | null }> = ({ stage }) => { - if (!stage) { - return Pending; - } + const isCancellable = canCancelOrder(order); - return {stage}; -}; + const handleCancelClick = (): void => { + setCancelDialogOpen(true); + }; -const OrderTableRow: React.FC<{ order: TerminalProductOrder }> = observer( - ({ order }) => { - const [expanded, setExpanded] = React.useState(false); + const formatCurrency = ( + amount: number | string | null | undefined, + ): string => { + const numAmount = + isNullable(amount) || amount === 'null' + ? 0 + : typeof amount === 'string' + ? parseFloat(amount) + : amount; + return numAmount.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }); + }; - return ( - <> - setExpanded(!expanded)}> - - - - {expanded ? ( - - ) : ( - - )} - - #{order.order_id} + return ( + <> + + } + aria-controls={`${order.order_id}-content`} + id={`${order.order_id}-header`} + className="legend-marketplace-order-accordion__summary" + > + + + + Order Placed + + + {formatOrderDate(order.created_at)} + - - - - + + + Total + + + {formatCurrency(order.order_cost)} + + - - - {order.created_at.split('T')[0]} - - + + + Order # + + + {order.order_id} + + - - - {order.service_pricing_items.length} - - + {isOpenOrder && ( + + + + + + )} + + - - - {order.order_cost} - - + + + + + {order.service_pricing_items.map((item, index) => ( + + + + {order.vendor_name} + + + + + + + + + {item.entity_name} + + {index === order.service_pricing_items.length - 1 && + order.business_justification && ( + + Business Justification: {order.business_justification} + + )} + + ))} + + - - - - + + {order.workflow_details && } + + + + - - - - - - Order Items ({order.service_pricing_items.length}) - - - - - - - Product - - - Provider - - - Category - - - - - {order.order_category === OrderCategory.TERMINAL ? ( - - - - {order.vendor_profile_name} - - - - - {order.vendor_name} - - - - - {'Terminal'} - - - - ) : ( - order.service_pricing_items.map((item, index) => ( - - - - {item.service_pricing_name} - - - - - {order.vendor_name} - - - - - {item.service_pricing_id === - order.vendor_profile_id - ? 'Terminal' - : 'Add-on'} - - - - )) - )} - -
-
-
-
-
-
- - ); - }, -); + setCancelDialogOpen(false)} + order={order} + orderStore={ordersStore} + /> + + ); +}); export const LegendMarketplaceYourOrders: React.FC = withLegendMarketplaceOrdersStore( @@ -243,7 +254,6 @@ export const LegendMarketplaceYourOrders: React.FC = const baseStore = useLegendMarketplaceBaseStore(); const ordersStore = useLegendMarketplaceOrdersStore(); - // Helper function to safely execute flow operations const executeFlowSafely = useCallback( (flowFn: () => Generator, void, unknown>) => { flowResult(flowFn()).catch((error) => { @@ -272,12 +282,7 @@ export const LegendMarketplaceYourOrders: React.FC = [ordersStore, executeFlowSafely], ); - const handleRefresh = useCallback(() => { - executeFlowSafely(() => ordersStore.refreshCurrentOrders()); - }, [ordersStore, executeFlowSafely]); - useEffect(() => { - // Load open orders by default if (ordersStore.openOrders.length === 0) { executeFlowSafely(() => ordersStore.fetchOpenOrders()); } @@ -291,20 +296,9 @@ export const LegendMarketplaceYourOrders: React.FC = Your Orders - - + {isLoading ? ( - + - + Loading your orders... ) : currentOrders.length === 0 ? ( - - - + + + No{' '} {ordersStore.selectedTab === 'open' ? 'active' : 'completed'}{' '} orders found - + {ordersStore.selectedTab === 'open' ? "You don't have any orders in progress. Start shopping to place your first order!" : "You don't have any completed orders yet. Your completed orders will appear here."} ) : ( - - - - - Order ID - Status - Order Date - Items - Total Cost - Stage - - - - {currentOrders.map((order) => ( - - ))} - -
-
+ {currentOrders.map((order) => { + const isOpenOrder = + order.workflow_details?.workflow_status === + OrderStatus.IN_PROGRESS || + order.workflow_details?.workflow_status === + OrderStatus.OPEN; + return ( + + ); + })} + )}
diff --git a/packages/legend-application-marketplace/src/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.tsx b/packages/legend-application-marketplace/src/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.tsx index 45d58958ed6..47139b67136 100644 --- a/packages/legend-application-marketplace/src/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.tsx +++ b/packages/legend-application-marketplace/src/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.tsx @@ -27,15 +27,20 @@ import { List, ListItem, CircularProgress, + Pagination, + Box, + Select, + MenuItem, + type SelectChangeEvent, } from '@mui/material'; -import type { Filter, TerminalResult } from '@finos/legend-server-marketplace'; +import type { TerminalResult } from '@finos/legend-server-marketplace'; import { LegendMarketplaceTerminalCard } from '../../components/ProviderCard/LegendMarketplaceTerminalCard.js'; import { type LegendMarketPlaceVendorDataStore, VendorDataProviderType, } from '../../stores/LegendMarketPlaceVendorDataStore.js'; import { LegendMarketplacePage } from '../LegendMarketplacePage.js'; -import { useEffect } from 'react'; +import { useEffect, useCallback } from 'react'; import { useLegendMarketPlaceVendorDataStore, withLegendMarketplaceVendorDataStore, @@ -49,6 +54,7 @@ import { SparkleStarsIcon, } from '@finos/legend-art'; import { ComingSoonDisplay } from '../../components/ComingSoon/ComingSoonDisplay.js'; +import { flowResult } from 'mobx'; export const RefinedVendorRadioSelector = observer( (props: { vendorDataState: LegendMarketPlaceVendorDataStore }) => { @@ -59,14 +65,15 @@ export const RefinedVendorRadioSelector = observer( VendorDataProviderType.ADD_ONS, ]; - const onRadioChange = (value: VendorDataProviderType) => { - vendorDataState.setProviderDisplayState(value); - if (value === VendorDataProviderType.TERMINAL_LICENSE) { - vendorDataState.setProviders('desktop'); - } else { - vendorDataState.setProviders('addon'); - } - }; + const onRadioChange = useCallback( + (value: VendorDataProviderType) => { + vendorDataState.setProviderDisplayState(value); + flowResult(vendorDataState.populateProviders()).catch( + vendorDataState.applicationStore.alertUnhandledError, + ); + }, + [vendorDataState], + ); return ( @@ -142,6 +149,95 @@ const SearchResultsRenderer = observer( }, ); +const PaginationControls = observer( + (props: { vendorDataState: LegendMarketPlaceVendorDataStore }) => { + const { vendorDataState } = props; + + const totalPages = Math.ceil( + vendorDataState.totalItems / vendorDataState.itemsPerPage, + ); + + const handlePageChange = useCallback( + (_event: React.ChangeEvent, page: number) => { + vendorDataState.setPage(page); + flowResult(vendorDataState.populateProviders()).catch( + vendorDataState.applicationStore.alertUnhandledError, + ); + }, + [vendorDataState], + ); + + const handleItemsPerPageChange = useCallback( + (event: SelectChangeEvent) => { + vendorDataState.setItemsPerPage(Number(event.target.value)); + flowResult(vendorDataState.populateProviders()).catch( + vendorDataState.applicationStore.alertUnhandledError, + ); + }, + [vendorDataState], + ); + + if (vendorDataState.providers.length === 0) { + return null; + } + + return ( + + + + Items per page: + + + + + + Showing{' '} + + {(vendorDataState.page - 1) * vendorDataState.itemsPerPage + 1} + {' '} + to{' '} + + {Math.min( + vendorDataState.page * vendorDataState.itemsPerPage, + vendorDataState.totalItems, + )} + {' '} + of {vendorDataState.totalItems} results + + + + + + + + ); + }, +); + export const VendorDataMainContent = observer( (props: { marketPlaceVendorDataState: LegendMarketPlaceVendorDataStore }) => { const { marketPlaceVendorDataState } = props; @@ -157,7 +253,7 @@ export const VendorDataMainContent = observer( ) : ( <> -
+
Filters
@@ -225,6 +321,14 @@ export const VendorDataMainContent = observer( /> )}
+ {(marketPlaceVendorDataState.providerDisplayState === + VendorDataProviderType.TERMINAL_LICENSE || + marketPlaceVendorDataState.providerDisplayState === + VendorDataProviderType.ADD_ONS) && ( + + )} )}
@@ -236,11 +340,27 @@ export const LegendMarketplaceVendorData = withLegendMarketplaceVendorDataStore( observer(() => { const marketPlaceVendorDataStore = useLegendMarketPlaceVendorDataStore(); - const onChange = (query: string | undefined) => { - marketPlaceVendorDataStore.setProvidersFilters([ - { label: 'query', value: query }, - ] as Filter[]); - }; + const handleSearch = useCallback( + (query: string | undefined) => { + marketPlaceVendorDataStore.setSearchTerm(query ?? ''); + flowResult(marketPlaceVendorDataStore.populateProviders()).catch( + marketPlaceVendorDataStore.applicationStore.alertUnhandledError, + ); + }, + [marketPlaceVendorDataStore], + ); + + const handleSearchChange = useCallback( + (query: string) => { + if (query === '') { + marketPlaceVendorDataStore.setSearchTerm(''); + flowResult(marketPlaceVendorDataStore.populateProviders()).catch( + marketPlaceVendorDataStore.applicationStore.alertUnhandledError, + ); + } + }, + [marketPlaceVendorDataStore], + ); useEffect(() => { marketPlaceVendorDataStore.init(); @@ -250,7 +370,10 @@ export const LegendMarketplaceVendorData = withLegendMarketplaceVendorDataStore(
- +
diff --git a/packages/legend-application-marketplace/src/stores/LegendMarketPlaceVendorDataStore.tsx b/packages/legend-application-marketplace/src/stores/LegendMarketPlaceVendorDataStore.tsx index dd40cfcc397..6308698fc9d 100644 --- a/packages/legend-application-marketplace/src/stores/LegendMarketPlaceVendorDataStore.tsx +++ b/packages/legend-application-marketplace/src/stores/LegendMarketPlaceVendorDataStore.tsx @@ -18,14 +18,19 @@ import { TerminalResult, type Filter, type MarketplaceServerClient, + type TerminalServicesResponse, + ProductType, } from '@finos/legend-server-marketplace'; -import { action, flow, makeObservable, observable } from 'mobx'; +import { action, flow, flowResult, makeObservable, observable } from 'mobx'; import type { LegendMarketplaceApplicationStore, LegendMarketplaceBaseStore, } from './LegendMarketplaceBaseStore.js'; -import { ActionState, type GeneratorFn } from '@finos/legend-shared'; -import { toastManager } from '../components/Toast/CartToast.js'; +import { + ActionState, + type GeneratorFn, + assertErrorThrown, +} from '@finos/legend-shared'; export enum VendorDataProviderType { ALL = 'All', @@ -33,22 +38,52 @@ export enum VendorDataProviderType { ADD_ONS = 'Add-Ons', } +export class FetchProductsParams { + kerberos: string; + product_type: ProductType; + preferred_products: boolean; + page_size: number; + search: string; + page_number: number | undefined; + + constructor( + kerberos: string, + productType: ProductType, + preferredProducts: boolean, + pageSize: number, + search: string, + pageNumber?: number, + ) { + this.kerberos = kerberos; + this.product_type = productType; + this.preferred_products = preferredProducts; + this.page_size = pageSize; + this.search = search; + this.page_number = pageNumber; + } +} + export class LegendMarketPlaceVendorDataStore { readonly applicationStore: LegendMarketplaceApplicationStore; readonly store: LegendMarketplaceBaseStore; marketplaceServerClient: MarketplaceServerClient; - responseLimit = 6; - currentUser = ''; readonly fetchingProvidersState = ActionState.create(); - //Vendor Data Page terminalProviders: TerminalResult[] = []; addOnProviders: TerminalResult[] = []; providers: TerminalResult[] = []; + page = 1; + itemsPerPage = 24; + totalTerminalItems = 0; + totalAddOnItems = 0; + totalItems = 0; + + searchTerm = ''; + providersFilters: Filter[] = []; providerDisplayState: VendorDataProviderType = VendorDataProviderType.ALL; @@ -60,14 +95,22 @@ export class LegendMarketPlaceVendorDataStore { makeObservable(this, { terminalProviders: observable, addOnProviders: observable, - populateProviders: action, - providerDisplayState: observable, - setProviderDisplayState: action, providers: observable, - setProviders: action, - init: flow, + page: observable, + itemsPerPage: observable, + totalTerminalItems: observable, + totalAddOnItems: observable, + totalItems: observable, + searchTerm: observable, + providerDisplayState: observable, providersFilters: observable, + setProviderDisplayState: action, setProvidersFilters: action, + setPage: action, + setItemsPerPage: action, + setSearchTerm: action, + init: flow, + populateProviders: flow, }); this.applicationStore = applicationStore; @@ -85,101 +128,112 @@ export class LegendMarketPlaceVendorDataStore { } try { - yield this.populateProviders(); + yield flowResult(this.populateProviders()); } catch (error) { + assertErrorThrown(error); this.applicationStore.notificationService.notifyError( - `Failed to initialize vendors: ${error}`, + `Failed to initialize vendors: ${error.message}`, ); } } setProviderDisplayState(value: VendorDataProviderType): void { this.providerDisplayState = value; + this.page = 1; } setProvidersFilters(value: Filter[]): void { this.providersFilters = value; - this.populateData(); + this.page = 1; } - populateData(): void { - this.populateProviders() - .then(() => - this.applicationStore.notificationService.notifySuccess( - 'Data populated successfully.', - ), - ) - .catch((error: Error) => - this.applicationStore.notificationService.notifyError( - `Failed to populate Data: ${ - error instanceof Error ? error.message : String(error) - }`, - ), - ); + setPage(value: number): void { + this.page = value; } - async populateProviders(): Promise { - try { - const filters: string = this.providersFilters - .map((filter) => `&${filter.label}=${encodeURIComponent(filter.value)}`) - .join(''); + setItemsPerPage(value: number): void { + this.itemsPerPage = value; + this.page = 1; + } + + setSearchTerm(value: string): void { + this.searchTerm = value; + this.page = 1; + } + *populateProviders(): GeneratorFn { + try { this.fetchingProvidersState.inProgress(); - this.terminalProviders = ( - await this.marketplaceServerClient.getVendorsByCategory( + if (this.providerDisplayState === VendorDataProviderType.ALL) { + const params = new FetchProductsParams( + this.currentUser, + ProductType.ALL, + true, + this.itemsPerPage, + this.searchTerm, + ); + const response = (yield this.marketplaceServerClient.fetchProducts( + params, + )) as TerminalServicesResponse; + + this.terminalProviders = (response.vendor_profiles ?? []).map((json) => + TerminalResult.serialization.fromJson(json), + ); + + this.addOnProviders = (response.service_pricing ?? []).map((json) => + TerminalResult.serialization.fromJson(json), + ); + + this.totalTerminalItems = response.vendor_profiles_total_count ?? 0; + this.totalAddOnItems = response.service_pricing_total_count ?? 0; + } else if ( + this.providerDisplayState === VendorDataProviderType.TERMINAL_LICENSE + ) { + const params = new FetchProductsParams( this.currentUser, - encodeURIComponent('desktop'), - 'landing', - filters, - this.responseLimit, - ) - ).map((json) => { - return TerminalResult.serialization.fromJson(json); - }); - - this.addOnProviders = ( - await this.marketplaceServerClient.getVendorsByCategory( + ProductType.VENDOR_PROFILE, + false, + this.itemsPerPage, + this.searchTerm, + this.page, + ); + const response = (yield this.marketplaceServerClient.fetchProducts( + params, + )) as TerminalServicesResponse; + + this.providers = (response.vendor_profiles ?? []).map((json) => + TerminalResult.serialization.fromJson(json), + ); + + this.totalItems = response.total_count ?? 0; + } else { + const params = new FetchProductsParams( this.currentUser, - encodeURIComponent('addon'), - 'landing', - filters, - this.responseLimit, - ) - ).map((json) => TerminalResult.serialization.fromJson(json)); + ProductType.SERVICE_PRICING, + false, + this.itemsPerPage, + this.searchTerm, + this.page, + ); + const response = (yield this.marketplaceServerClient.fetchProducts( + params, + )) as TerminalServicesResponse; + + this.providers = (response.service_pricing ?? []).map((json) => + TerminalResult.serialization.fromJson(json), + ); + + this.totalItems = response.total_count ?? 0; + } + + this.fetchingProvidersState.complete(); } catch (error) { + assertErrorThrown(error); this.applicationStore.notificationService.notifyError( - `Failed to fetch vendors: ${error}`, + `Failed to fetch vendors: ${error.message}`, ); - } finally { - this.fetchingProvidersState.complete(); + this.fetchingProvidersState.fail(); } } - - setProviders(category: string): void { - this.providers = []; - const filters: string = this.providersFilters - .map((filter) => `&${filter.label}=${encodeURIComponent(filter.value)}`) - .join(''); - - this.fetchingProvidersState.inProgress(); - this.marketplaceServerClient - .getVendorsByCategory( - this.currentUser, - encodeURIComponent(category), - 'list', - filters, - this.responseLimit, - ) - .then((response) => { - this.providers = response.map((json) => { - return TerminalResult.serialization.fromJson(json); - }); - this.fetchingProvidersState.complete(); - }) - .catch((error) => { - toastManager.error(`Failed to fetch vendors: ${error.message}`); - this.fetchingProvidersState.fail(); - }); - } } diff --git a/packages/legend-application-marketplace/src/stores/cart/CartStore.ts b/packages/legend-application-marketplace/src/stores/cart/CartStore.ts index fa39aafad01..0b64fbdafc1 100644 --- a/packages/legend-application-marketplace/src/stores/cart/CartStore.ts +++ b/packages/legend-application-marketplace/src/stores/cart/CartStore.ts @@ -145,7 +145,8 @@ export class CartStore { price: provider.price, description: provider.description, isOwned: provider.isOwned ? 'true' : 'false', - vendorProfileId: provider.vendorProfileId ?? provider.id, + model: provider.model ?? provider.productName, + skipWorkflow: provider.skipWorkflow ?? false, }; } diff --git a/packages/legend-application-marketplace/src/stores/orders/OrderHelpers.ts b/packages/legend-application-marketplace/src/stores/orders/OrderHelpers.ts new file mode 100644 index 00000000000..367821fc1ec --- /dev/null +++ b/packages/legend-application-marketplace/src/stores/orders/OrderHelpers.ts @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2025-present, Goldman Sachs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { TerminalProductOrder } from '@finos/legend-server-marketplace'; + +export enum WorkflowStage { + ORDER_PLACED = 'Order Placed', + MANAGER_APPROVAL = 'Manager Approval', + BUSINESS_ANALYST_APPROVAL = 'Business Analyst Approval', + PENDING_FULFILLMENT = 'Pending Fulfillment', + CANCELLED = 'Cancelled', +} + +export enum WorkflowStatus { + COMPLETED = 'COMPLETED', +} + +export enum OrderType { + CANCELLATION = 'CANCELLATION', +} + +export enum WorkflowCurrentStage { + DIRECT_MANAGER = 'DIRECT MANAGER', + BUSINESS_ANALYST = 'Business Analyst', + RPM = 'RPM', +} + +export enum RejectedActionStatus { + REJECTED = 'rejected', + CANCELLED = 'cancelled', + AUTO_CANCELLED = 'auto cancelled', + DENIED = 'denied', +} + +export const STAGE_MAP: Record = { + [WorkflowCurrentStage.DIRECT_MANAGER]: WorkflowStage.MANAGER_APPROVAL, + [WorkflowCurrentStage.BUSINESS_ANALYST]: + WorkflowStage.BUSINESS_ANALYST_APPROVAL, + [WorkflowCurrentStage.RPM]: WorkflowStage.PENDING_FULFILLMENT, +}; + +export const getWorkflowSteps = ( + order: TerminalProductOrder, +): WorkflowStage[] => { + if (order.order_type.toUpperCase() === OrderType.CANCELLATION) { + return [ + WorkflowStage.ORDER_PLACED, + WorkflowStage.MANAGER_APPROVAL, + WorkflowStage.CANCELLED, + ]; + } + + return [ + WorkflowStage.ORDER_PLACED, + WorkflowStage.MANAGER_APPROVAL, + WorkflowStage.PENDING_FULFILLMENT, + ]; +}; + +export const getProcessInstanceId = ( + order: TerminalProductOrder, +): string | null => { + if (!order.workflow_details) { + return null; + } + + if ( + order.workflow_details.current_stage === WorkflowCurrentStage.DIRECT_MANAGER + ) { + return order.workflow_details.manager_process_id; + } else if ( + order.workflow_details.current_stage === + WorkflowCurrentStage.BUSINESS_ANALYST + ) { + return order.workflow_details.bbg_approval_process_id; + } + return null; +}; + +export const canCancelOrder = (order: TerminalProductOrder): boolean => { + const currentStage = order.workflow_details?.current_stage; + return ( + currentStage === WorkflowCurrentStage.DIRECT_MANAGER || + currentStage === WorkflowCurrentStage.BUSINESS_ANALYST + ); +}; + +export const isStageCompleted = ( + order: TerminalProductOrder, + stageName: string, +): boolean => { + if (!order.workflow_details) { + return false; + } + + if (stageName === WorkflowStage.MANAGER_APPROVAL) { + return !!order.workflow_details.manager_actioned_by; + } else if (stageName === WorkflowStage.BUSINESS_ANALYST_APPROVAL) { + return !!order.workflow_details.bbg_approval_actioned_by; + } + return false; +}; + +export const isStageRejected = ( + order: TerminalProductOrder, + stageName: string, +): boolean => { + if (!order.workflow_details) { + return false; + } + + const rejectedStatuses = Object.values(RejectedActionStatus); + + if (stageName === WorkflowStage.MANAGER_APPROVAL) { + return rejectedStatuses.some((status) => + order.workflow_details?.manager_action + ?.toLowerCase() + .includes(status.toLowerCase()), + ); + } else if (stageName === WorkflowStage.BUSINESS_ANALYST_APPROVAL) { + return rejectedStatuses.some((status) => + order.workflow_details?.bbg_approval_action + ?.toLowerCase() + .includes(status.toLowerCase()), + ); + } else if (stageName === WorkflowStage.PENDING_FULFILLMENT) { + return rejectedStatuses.some((status) => + order.workflow_details?.rpm_action + ?.toLowerCase() + .includes(status.toLowerCase()), + ); + } + return false; +}; + +export const formatOrderDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +}; + +export const formatTimestamp = (timestamp: string): string => { + const date = new Date(timestamp); + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +}; diff --git a/packages/legend-application-marketplace/src/stores/orders/OrderStore.ts b/packages/legend-application-marketplace/src/stores/orders/OrderStore.ts index 996ae7a5766..54386e184b1 100644 --- a/packages/legend-application-marketplace/src/stores/orders/OrderStore.ts +++ b/packages/legend-application-marketplace/src/stores/orders/OrderStore.ts @@ -22,6 +22,7 @@ import { ActionState, } from '@finos/legend-shared'; import { APPLICATION_EVENT } from '@finos/legend-application'; +import { LEGEND_MARKETPLACE_APP_EVENT } from '../../__lib__/LegendMarketplaceAppEvent.js'; import type { LegendMarketplaceBaseStore } from '../LegendMarketplaceBaseStore.js'; import { OrderStatusCategory, @@ -38,6 +39,7 @@ export class OrdersStore { totalClosed = 0; readonly fetchOpenOrdersState = ActionState.create(); readonly fetchClosedOrdersState = ActionState.create(); + readonly cancelOrderState = ActionState.create(); selectedTab: 'open' | 'closed' = 'open'; constructor(baseStore: LegendMarketplaceBaseStore) { @@ -50,6 +52,7 @@ export class OrdersStore { setSelectedTab: action, fetchOpenOrders: flow, fetchClosedOrders: flow, + cancelOrder: flow, currentOrders: computed, currentFetchState: computed, }); @@ -139,4 +142,52 @@ export class OrdersStore { yield* this.fetchClosedOrders(); } } + + *cancelOrder( + orderId: string, + processInstanceId: string, + comments?: string, + ): GeneratorFn { + const user = this.baseStore.applicationStore.identityService.currentUser; + + if (!user) { + this.baseStore.applicationStore.notificationService.notifyError( + 'User not authenticated', + ); + return false; + } + + this.cancelOrderState.inProgress(); + try { + yield this.baseStore.marketplaceServerClient.cancelOrder({ + order_id: orderId, + kerberos: user, + comments: comments ?? '', + process_instance_id: processInstanceId, + }); + + this.baseStore.applicationStore.notificationService.notifySuccess( + `Order #${orderId} cancelled successfully`, + ); + this.cancelOrderState.complete(); + + // Refresh orders after successful cancellation + yield* this.refreshCurrentOrders(); + + return true; + } catch (error) { + assertErrorThrown(error); + this.baseStore.applicationStore.logService.error( + LogEvent.create( + LEGEND_MARKETPLACE_APP_EVENT.ORDER_CANCELLATION_FAILURE, + ), + `Failed to cancel order: ${error.message}`, + ); + this.baseStore.applicationStore.notificationService.notifyError( + `Failed to cancel order: ${error.message}`, + ); + this.cancelOrderState.fail(); + return false; + } + } } diff --git a/packages/legend-application-marketplace/style/components/_legend-marketplace-card.scss b/packages/legend-application-marketplace/style/components/_legend-marketplace-card.scss index b792079f788..a0c01b1afd9 100644 --- a/packages/legend-application-marketplace/style/components/_legend-marketplace-card.scss +++ b/packages/legend-application-marketplace/style/components/_legend-marketplace-card.scss @@ -195,7 +195,7 @@ &__content { position: absolute; bottom: 0; - height: 55%; + height: 45%; background-color: rgb(11 11 11 / 70%); overflow: hidden; text-overflow: ellipsis; @@ -204,17 +204,26 @@ padding: 0.5rem 1.2rem; } - &__title { - font-size: 3rem; - font-family: 'GS Sans Condensed', 'Trebuchet MS', sans-serif; - margin-bottom: 0.5rem; - word-wrap: break-word; + &__provider { + font: + 500 1.8rem / 1.8rem 'GS Sans Condensed', + 'Trebuchet MS', + sans-serif; color: var(--color-white); + margin-bottom: 1rem; + word-wrap: break-word; opacity: 0.95; + text-transform: uppercase; + letter-spacing: 0.05rem; } - &__description { - font-size: 1.6rem; + &__title { + font: + 2.75rem / 2.75rem 'GS Sans Condensed', + 'Trebuchet MS', + sans-serif; + margin-bottom: 0.5rem; + word-wrap: break-word; color: var(--color-white); opacity: 0.95; } @@ -262,4 +271,16 @@ background-color: var(--color-blue-400); } } + + &__category-chip { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 1; + background-color: var(--color-dark-grey-100); + color: var(--color-white); + font-size: 1.2rem; + font-weight: 600; + border: 1px solid var(--color-white); + } } diff --git a/packages/legend-application-marketplace/style/components/_recommended-addons-modal.scss b/packages/legend-application-marketplace/style/components/_recommended-addons-modal.scss index 9cb9d5df335..a31a5346750 100644 --- a/packages/legend-application-marketplace/style/components/_recommended-addons-modal.scss +++ b/packages/legend-application-marketplace/style/components/_recommended-addons-modal.scss @@ -129,6 +129,83 @@ font-weight: 400; } + &__filter-controls { + display: flex; + justify-content: space-between; + align-items: center; + gap: 2rem; + margin-bottom: 2rem; + } + + &__search-field { + flex: 1; + max-width: 40rem; + + .MuiOutlinedInput-root { + border-radius: 1.2rem; + background: var(--color-hclight-grey-5); + transition: all 0.3s ease; + + &:hover { + background: var(--color-hclight-grey-10); + } + + &.Mui-focused { + background: rgb(255 255 255 / 100%); + box-shadow: 0 4px 12px rgb(0 0 0 / 8%); + } + } + + .MuiInputBase-input { + font-size: 1.4rem; + padding: 1.2rem 1.6rem; + } + } + + &__sort-buttons { + border-radius: 1.2rem; + overflow: hidden; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + + .MuiToggleButton-root { + border: 1px solid var(--color-hclight-grey-20); + padding: 1rem 1.6rem; + font-size: 1.3rem; + font-weight: 500; + color: var(--color-hclight-grey-70); + background: var(--color-hclight-grey-5); + transition: all 0.3s ease; + + &:hover { + background: var(--color-hclight-grey-10); + } + + &.Mui-selected { + background: linear-gradient( + 135deg, + rgb(59 130 246 / 12%) 0%, + rgb(99 102 241 / 12%) 100% + ); + color: var(--color-primary); + font-weight: 600; + border-color: rgb(59 130 246 / 30%); + + &:hover { + background: linear-gradient( + 135deg, + rgb(59 130 246 / 18%) 0%, + rgb(99 102 241 / 18%) 100% + ); + } + } + + svg { + width: 1.6rem; + height: 1.6rem; + } + } + } + &__empty-state { display: flex; flex-direction: column; @@ -149,194 +226,96 @@ } } - &__addon-card { - height: 100%; + &__list { display: flex; flex-direction: column; - background: rgb(255 255 255 / 70%); - backdrop-filter: blur(8px); - border: 1px solid rgb(226 232 240 / 80%); - overflow: hidden; - position: relative; + gap: 0; + } - &::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 2px; - background: linear-gradient( - 90deg, - transparent 0%, - var(--color-modal-blue-focus) 50%, - transparent 100% - ); - } + &__list-header { + display: grid; + grid-template-columns: 2fr 1.5fr 1fr 1.5fr; + gap: 2rem; + padding: 1.2rem 2rem; + background: var(--color-hclight-grey-10); + border-radius: 0.8rem; + margin-bottom: 1rem; - &:hover { - border-color: var(--color-modal-blue-light); - box-shadow: 0 4px 8px rgb(147 197 253 / 15%); + .MuiTypography-root { + font-size: 1.3rem; + font-weight: 600; + color: var(--color-hclight-grey-70); + text-transform: uppercase; + letter-spacing: 0.05rem; } } - &__card-content { - flex: 1; - padding: 2rem; + &__header-name, + &__header-provider, + &__header-price { + display: flex; + align-items: center; } - &__card-header { + &__header-action { display: flex; - align-items: flex-start; - gap: 1rem; - margin-bottom: 1.5rem; + align-items: center; + justify-content: center; } - &__product-name { - font-size: 1.6rem; - font-weight: 700; - flex: 1; - line-height: 1.3; - color: var(--color-modal-text-primary); - margin: 0; - } + &__list-item { + display: grid; + grid-template-columns: 2fr 1.5fr 1fr 1.5fr; + gap: 2rem; + padding: 1.8rem 2rem; + align-items: center; + transition: background-color 0.2s ease; - &__provider-chip { - background: linear-gradient( - 135deg, - var(--color-modal-background-medium), - var(--color-modal-border-light) - ); - color: var(--color-modal-text-muted); - font-size: 1.1rem; - font-weight: 600; - height: auto; - padding: 0.4rem 1rem; - border-radius: 12px; - border: 1px solid rgb(148 163 184 / 20%); + &:hover { + background: var(--color-hclight-grey-5); + } } - &__description { - margin-bottom: 1.5rem; - line-height: 1.6; - color: var(--color-modal-text-secondary); - font-size: 1.3rem; - font-weight: 400; + &__item-name { + font-size: 1.5rem; + font-weight: 600; + color: var(--color-hclight-black); + line-height: 1.4; } - &__card-footer { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: auto; + &__item-provider { + font-size: 1.4rem; + font-weight: 500; + color: var(--color-hclight-blue-60); } - &__category-chip { - background: linear-gradient( - 135deg, - rgb(99 102 241 / 10%), - rgb(168 85 247 / 10%) - ); - color: var(--color-modal-purple-secondary); - font-size: 1.1rem; + &__item-price { + font-size: 1.4rem; font-weight: 600; - height: auto; - padding: 0.5rem 1.2rem; - border-radius: 16px; - border: 1px solid rgb(99 102 241 / 20%); + color: var(--color-hclight-green-50); } - &__price { - font-size: 1.8rem; - font-weight: 800; - background: linear-gradient( - 135deg, - var(--color-modal-blue-focus), - var(--color-modal-purple-primary) - ); - background-clip: text; - -webkit-text-fill-color: transparent; - - &__free { - background: linear-gradient( - 135deg, - var(--color-modal-success-primary), - var(--color-modal-success-secondary) - ); - background-clip: text; - -webkit-text-fill-color: transparent; - } - } - - &__card-actions { - padding: 0 2rem 2rem; + &__item-action { + display: flex; + justify-content: center; } &__add-btn { - background: linear-gradient( - 135deg, - var(--color-modal-blue-primary), - var(--color-modal-blue-secondary) - ); - color: var(--color-white); - text-transform: none; - font-weight: 600; font-size: 1.3rem; - padding: 1rem 2rem; - border-radius: 16px; - border: none; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; - - &::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient( - 90deg, - transparent 0%, - rgb(255 255 255 / 20%) 50%, - transparent 100% - ); - transition: left 0.6s ease; - } - - &:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 12px 24px -6px rgb(59 130 246 / 40%); - - &::before { - left: 100%; - } - } - - &__added { - background: linear-gradient( - 135deg, - rgb(34 197 94 / 10%), - rgb(22 163 74 / 10%) - ); - border: 2px solid var(--color-modal-success-primary); - color: var(--color-modal-success-secondary); + font-weight: 600; + padding: 0.8rem 1.6rem; + border-radius: 0.6rem; + text-transform: none; + transition: all 0.2s ease; - &:hover:not(:disabled) { - background: linear-gradient( - 135deg, - rgb(34 197 94 / 15%), - rgb(22 163 74 / 15%) - ); - transform: none; - box-shadow: none; - } + &--added { + background: var(--color-hclight-green-50); + color: var(--color-white); + border-color: var(--color-hclight-green-50); - &:disabled { - border-color: var(--color-modal-success-primary); - color: var(--color-modal-success-secondary); - opacity: 0.8; + &:hover { + background: var(--color-hclight-green-60); + border-color: var(--color-hclight-green-60); } } } @@ -481,25 +460,5 @@ order: 1; } } - - &__card-content { - padding: 1.5rem; - } - - &__card-actions { - padding: 0 1.5rem 1.5rem; - } - - &__product-name { - font-size: 1.4rem; - } - - &__description { - font-size: 1.2rem; - } - - &__price { - font-size: 1.6rem; - } } } diff --git a/packages/legend-application-marketplace/style/pages/_vendor-data.scss b/packages/legend-application-marketplace/style/pages/_vendor-data.scss index 3976ad73599..7822b4003bc 100644 --- a/packages/legend-application-marketplace/style/pages/_vendor-data.scss +++ b/packages/legend-application-marketplace/style/pages/_vendor-data.scss @@ -19,7 +19,6 @@ .legend-marketplace-vendor-data { width: 100%; height: 100%; - overflow: auto; &__content { flex-direction: column; @@ -29,11 +28,11 @@ } .legend-marketplace-banner { - background: #7297c5; + background: var(--legend-marketplace-light-blue); width: 100%; height: 195px; padding: 20px; - color: white; + color: var(--color-white); display: flex; justify-content: center; align-items: center; @@ -75,7 +74,7 @@ .legend-marketplace-vendordata-main { display: flex; - flex-direction: row; + flex-direction: column; width: 100%; gap: 10px; @@ -89,11 +88,24 @@ gap: 2rem; } + &__content-wrapper { + display: flex; + flex-direction: row; + width: 100%; + } + .legend-marketplace-vendordata-main-sidebar { - width: 15%; + width: 10%; padding: 20px; margin-right: 20px; + &--hidden { + display: none; + width: 10%; + padding: 0; + margin-right: 0; + } + &__title { margin-bottom: 10px; font-size: 1.5em; @@ -128,9 +140,10 @@ display: flex; flex-direction: column; gap: 1em; - width: 100%; padding: 20px; - padding-bottom: 100px; + padding-bottom: 50px; + margin-left: 25rem; + flex: 1; &__category { display: flex; @@ -143,7 +156,7 @@ .see-all { text-decoration: none; - color: #186ade; + color: var(--color-blue-50); align-items: center; margin-top: 1.1rem; } @@ -154,6 +167,177 @@ flex-wrap: wrap; gap: 1rem; margin-top: 1rem; + justify-content: flex-start; + width: 100%; + } + } + + .legend-marketplace-pagination-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + width: 100%; + max-width: 1920px; + margin: 3rem auto; + gap: 2rem; + padding: 2rem; + } + + .legend-marketplace-pagination-info { + display: flex; + align-items: center; + flex-shrink: 0; + white-space: nowrap; + + .MuiTypography-root { + font-weight: 500; + color: var(--color-dark-grey-400); + font-size: 2rem; + + strong { + color: var(--color-blue-100); + font-weight: 600; + } + } + } + + .legend-marketplace-pagination-controls { + display: flex; + justify-content: center; + align-items: center; + flex: 1; + flex-shrink: 1; + flex-wrap: wrap; + + .MuiPagination-ul { + gap: 0.5rem; + flex-wrap: wrap; + } + + .MuiPaginationItem-root { + font-size: 2rem; + font-weight: 500; + min-width: 56px; + height: 56px; + border-radius: 8px; + border: 1px solid var(--color-light-grey-200); + color: var(--color-dark-grey-300); + transition: all 0.2s ease; + + &:hover { + background-color: var(--color-light-grey-50); + border-color: var(--color-blue-100); + transform: translateY(-2px); + box-shadow: 0 2px 8px rgb(0 122 204 / 15%); + } + } + + .MuiPaginationItem-page.Mui-selected { + background-color: var(--color-blue-100); + color: var(--color-white); + border-color: var(--color-blue-100); + font-weight: 600; + box-shadow: 0 4px 12px rgb(0 122 204 / 30%); + + &:hover { + background-color: var(--color-blue-150); + border-color: var(--color-blue-150); + } + } + + .MuiPaginationItem-previousNext { + background-color: var(--color-white); + font-weight: 600; + min-width: 64px; + height: 64px; + font-size: 2.2rem; + + &:not(.Mui-disabled) { + color: var(--color-blue-100); + + &:hover { + background-color: var(--color-blue-100); + color: var(--color-white); + } + } + + &.Mui-disabled { + background-color: var(--color-light-grey-50); + color: var(--color-light-grey-400); + border-color: var(--color-light-grey-200); + } + } + + .MuiPaginationItem-firstLast { + background-color: var(--color-white); + font-weight: 600; + min-width: 64px; + height: 64px; + font-size: 2.2rem; + + &:not(.Mui-disabled) { + color: var(--color-blue-100); + + &:hover { + background-color: var(--color-blue-100); + color: var(--color-white); + } + } + + &.Mui-disabled { + background-color: var(--color-light-grey-50); + color: var(--color-light-grey-400); + border-color: var(--color-light-grey-200); + } + } + + .MuiPaginationItem-icon { + font-size: 2.8rem; + } + + .MuiPaginationItem-ellipsis { + border: none; + + &:hover { + background-color: transparent; + transform: none; + box-shadow: none; + } + } + } + + .legend-marketplace-pagination-page-size { + display: flex; + align-items: center; + flex-shrink: 0; + white-space: nowrap; + gap: 0.5rem; + + .MuiTypography-root { + font-weight: 500; + color: var(--color-dark-grey-400); + font-size: 2rem; + } + + .MuiSelect-select { + font-size: 2rem; + border-radius: 8px; + min-height: 1rem; + } + + .MuiOutlinedInput-notchedOutline { + border-color: var(--color-light-grey-200); + } + + .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline { + border-color: var(--color-blue-100); + } + + .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline { + border-color: var(--color-blue-100); + border-width: 2px; } } } diff --git a/packages/legend-application-marketplace/style/pages/_your-orders.scss b/packages/legend-application-marketplace/style/pages/_your-orders.scss index 056c9ae8ce3..b5c395e1fcf 100644 --- a/packages/legend-application-marketplace/style/pages/_your-orders.scss +++ b/packages/legend-application-marketplace/style/pages/_your-orders.scss @@ -1,72 +1,31 @@ -/** - * Copyright (c) 2025-present, Goldman Sachs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// Using CSS custom properties from legend-art variables - -// === Mixins === @use 'mixins' as *; @mixin transition-smooth { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } -@mixin hover-lift { - &:hover { - transform: translateY(-1px); - box-shadow: - 0 10px 15px -3px rgb(0 0 0 / 10%), - 0 4px 6px -4px rgb(0 0 0 / 10%); - } -} - -@mixin glass-effect { - background: rgb(255 255 255 / 80%); - backdrop-filter: blur(10px); - border: 1px solid rgb(255 255 255 / 20%); -} - -// === Main Container === .legend-marketplace-your-orders { background-color: var(--color-white); min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; &__header { - padding: 1.5rem 0 1rem; + width: 100%; + max-width: 1920px; + padding: 1.5rem 2rem 1rem; margin-bottom: 1.5rem; - .MuiBreadcrumbs-root { - font-size: 0.875rem; - font-weight: 500; - - .MuiLink-root { - color: rgb(255 255 255 / 90%); - text-decoration: none; - @include transition-smooth; - - &:hover { - color: var(--color-white); - text-decoration: underline; - } - } + @media (width <= 768px) { + padding: 1.5rem 1rem 1rem; } } &__content { width: 100%; - padding: 0 1.5rem 2rem; + max-width: 1920px; + padding: 0 2rem 2rem; background: var(--color-white); @media (width <= 768px) { @@ -89,411 +48,459 @@ } h1 { - font-size: 2.5rem; + font-size: 2.75rem; font-weight: 600; color: var(--color-dark-grey-100); } } - &__refresh-button { - border: 1px solid var(--color-light-grey-300) !important; - color: var(--color-dark-grey-400) !important; - font-weight: 500 !important; - font-size: 0.875rem !important; - padding: 0.5rem 1rem !important; - border-radius: 6px !important; - @include transition-smooth; + &__tabs { + border-bottom: 2px solid var(--color-light-grey-200); + margin-bottom: 2rem; - &:hover:not(:disabled) { - border-color: var(--color-blue-100) !important; - background-color: var(--color-light-grey-0) !important; - color: var(--color-blue-100) !important; - transform: translateY(-1px); - } + .MuiTab-root { + text-transform: none; + font-size: 2.25rem; + font-weight: 500; + min-height: 56px; + padding: 12px 24px; + color: var(--color-dark-grey-300); + @include transition-smooth; - &:disabled { - opacity: 0.7 !important; - cursor: not-allowed !important; - border-color: var(--color-light-grey-200) !important; - color: var(--color-dark-grey-500) !important; - background-color: var(--color-light-grey-100) !important; - } + &:hover { + color: var(--color-dark-grey-400); + } - .MuiSvgIcon-root { - font-size: 1rem; + &.Mui-selected { + color: var(--color-blue-100); + font-weight: 600; + } } - .MuiCircularProgress-root { - color: var(--color-blue-100) !important; + .MuiTabs-indicator { + height: 3px; + background-color: var(--color-blue-100); } } -} - -// === Tabs Styling === -.orders-tabs { - border-bottom: 1px solid var(--color-light-grey-200); - margin-bottom: 1.5rem; - - .MuiTab-root { - text-transform: none !important; - font-size: 2rem !important; - font-weight: 500 !important; - min-width: 120px !important; - padding: 0.75rem 1rem !important; - color: var(--color-dark-grey-500) !important; - @include transition-smooth; - &:hover { - color: var(--color-dark-grey-400) !important; - background-color: rgb(59 130 246 / 5%); - } - - &.Mui-selected { - color: var(--color-blue-100) !important; - font-weight: 600 !important; - } + &__orders-list { + margin-top: 1.5rem; } - .MuiTabs-indicator { - background-color: var(--color-blue-100) !important; - height: 2px !important; - border-radius: 1px !important; + &__loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1.5rem; + padding: 4rem 2rem; + color: var(--color-dark-grey-300); } -} -// === Loading State === -.orders-loading { - display: flex; - justify-content: center; - align-items: center; - padding: 3rem 0; - gap: 0.75rem; - - .MuiCircularProgress-root { - color: var(--color-blue-100); + &__loading-text { + font-size: 1.5rem; } - .loading-text { - font-size: 1rem; - color: var(--color-dark-grey-400); - font-weight: 500; + &__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + color: var(--color-dark-grey-300); + text-align: center; + gap: 1rem; } -} -// === Empty State === -.orders-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 3rem 1.5rem; - background-color: var(--color-light-grey-50); - border-radius: 1rem; - border: 1px dashed var(--color-light-grey-300); - text-align: center; - - .empty-icon { + &__empty-icon { + font-size: 4rem; + color: var(--color-light-grey-400); margin-bottom: 1rem; - opacity: 0.5; - color: var(--color-dark-grey-500); } - .empty-title { + &__empty-title { font-size: 2rem; font-weight: 600; - color: var(--color-dark-grey-100); + color: var(--color-dark-grey-200); margin-bottom: 0.5rem; } - .empty-description { - font-size: 1.3rem; - color: var(--color-dark-grey-500); - max-width: 380px; - line-height: 1.5; + &__empty-description { + font-size: 1.5rem; + color: var(--color-dark-grey-300); } } -// === Table Container === -.orders-table-container { - background: var(--color-white); - border-radius: 1rem !important; - box-shadow: - 0 1px 3px 0 rgb(0 0 0 / 10%), - 0 1px 2px -1px rgb(0 0 0 / 10%) !important; - border: 1px solid var(--color-light-grey-200) !important; - overflow: hidden; - - .MuiTable-root { - .MuiTableHead-root { - background-color: var(--color-light-grey-100); - - .MuiTableCell-root { - font-weight: 600 !important; - font-size: 1.5rem !important; - color: var(--color-dark-grey-400) !important; - text-transform: uppercase; - letter-spacing: 0.05em; - padding: 1rem 0.75rem !important; - border-bottom: 1px solid var(--color-light-grey-200) !important; - } - } +.legend-marketplace-order-accordion { + &__summary { + background-color: var(--color-light-grey-50); + border-radius: 8px; + padding: 1rem; + min-height: 60px; - .MuiTableBody-root { - .MuiTableRow-root { - @include transition-smooth; + &.Mui-expanded { + min-height: 60px; + } - cursor: pointer; + .MuiAccordionSummary-content { + margin: 0; + width: 100%; - &:hover { - background-color: rgb(59 130 246 / 2%) !important; - } + &.Mui-expanded { + margin: 0; + } + } + } - &:not(:last-child) { - border-bottom: 1px solid rgb(243 244 246 / 50%); - } + &__summary-content { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + width: 100%; + align-items: center; - .MuiTableCell-root { - font-size: 1.5rem; - vertical-align: middle; - } - } + @media (width <= 768px) { + gap: 1rem; } } -} -// === Order Status Chip === -.order-status-chip { - &--open { - background-color: rgb(245 158 11 / 10%) !important; - color: var(--color-yellow-400) !important; - font-weight: 500 !important; - font-size: 1.3rem !important; - padding: 0.25rem 0.5rem !important; - border-radius: 0.375rem !important; + &__summary-field { + flex: 1 1 150px; + display: flex; + flex-direction: column; + gap: 0.25rem; - .MuiSvgIcon-root { - font-size: 1.3rem; + @media (width <= 768px) { + flex: 1 1 120px; } } - &--closed { - background-color: rgb(34 197 94 / 10%) !important; - color: var(--color-green-500) !important; - font-weight: 500 !important; - font-size: 1.5rem !important; - padding: 0.25rem 0.5rem !important; - border-radius: 0.375rem !important; + &__summary-label { + font-size: 1.5rem; + color: var(--color-black); + text-transform: uppercase; + letter-spacing: 0.05em; + } + + &__summary-value { + font-size: 1.8rem; + font-weight: 600; + color: var(--color-dark-grey-100); + } + + &__summary-actions { + flex: 1 1 auto; + display: flex; + justify-content: flex-end; + margin-right: 1.6rem; + } + + &__cancel-button { + border: 1px solid var(--color-red-300); + color: var(--color-red-300); + font-weight: 500; + font-size: 1.5rem; + padding: 0.5rem 1rem; + border-radius: 6px; + text-transform: none; + @include transition-smooth; + + &:hover:not(:disabled) { + background-color: var(--color-red-0); + border-color: var(--color-red-300); + transform: translateY(-1px); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + border-color: var(--color-light-grey-300); + color: var(--color-dark-grey-500); + } .MuiSvgIcon-root { - font-size: 1.5rem; + font-size: 1.125rem; } } -} -// === Order Row Details === -.order-row { - &__id-cell { + &__details { + padding: 1.5rem; + background-color: var(--color-white); + } + + &__details-container { display: flex; + flex-wrap: wrap; + gap: 2rem; align-items: center; - gap: 0.375rem; - .expand-button { - padding: 0.25rem !important; - border-radius: 0.375rem !important; - @include transition-smooth; + @media (width <= 1024px) { + flex-direction: column; + align-items: stretch; + } + } - &:hover { - background-color: rgb(59 130 246 / 8%) !important; - } + &__items-section { + flex: 1 1 400px; + min-width: 0; + } - .MuiSvgIcon-root { - font-size: 1.5rem; - color: var(--color-dark-grey-500); - } - } + &__item { + padding-bottom: 1.5rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid var(--color-light-grey-200); - .order-id { - font-weight: 600 !important; - color: var(--color-blue-100) !important; - font-size: 1.5rem !important; + &:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; } } - &__date { - font-size: 1.5rem; - color: var(--color-dark-grey-400); - font-weight: 400; + &__vendor-chips-row { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.75rem; } - &__items-count { - font-weight: 600 !important; - font-size: 1.5rem !important; + &__vendor-name { + font-size: 1.5rem; color: var(--color-dark-grey-100); + font-weight: 500; } - &__total-cost { - font-weight: 600; - color: var(--color-green-500); + &__chips-container { + display: flex; + gap: 0.5rem; + } + + &__chip-terminal { + background-color: var(--color-order-chip-terminal-bg); + color: var(--color-order-chip-terminal-text); + font-weight: 700; font-size: 1.5rem; + height: auto; + padding: 4px 12px; } - &__timeline { + &__chip-addon { + background-color: var(--color-order-chip-addon-bg); + color: var(--color-order-chip-addon-text); + font-weight: 700; font-size: 1.5rem; - font-weight: 400; + height: auto; + padding: 4px 12px; + } - &--completed { - color: var(--color-green-500); - } + &__chip-category { + background-color: var(--color-order-chip-category-bg); + color: var(--color-order-chip-category-text); + font-weight: 700; + font-size: 1.5rem; + height: auto; + padding: 4px 12px; + } - &--estimated { - color: var(--color-dark-grey-400); - } + &__chip-price { + background-color: var(--color-order-chip-price-bg); + color: var(--color-order-chip-price-text); + font-weight: 700; + font-size: 1.5rem; + height: auto; + padding: 4px 12px; + border-radius: 16px; + border: none; + } - &--empty { - color: var(--color-dark-grey-500); + &__product-name { + font-size: 2.3rem; + font-weight: 600; + color: var(--color-blue-100); + text-decoration: none; + display: block; + margin-bottom: 0.75rem; + padding-top: 0.8rem; + + &:hover { + text-decoration: underline; } } - &__stage { - font-weight: 600 !important; - color: var(--color-white) !important; - font-size: 1.1rem !important; - background: linear-gradient( - 135deg, - var(--color-purple-100), - var(--color-magenta-100) - ) !important; - padding: 6px 12px !important; - border-radius: 20px !important; - border: none !important; - text-transform: capitalize !important; - box-shadow: 0 2px 4px rgb(0 0 0 / 10%) !important; - line-height: 1.2 !important; - text-align: center !important; - display: inline-block !important; + &__business-justification { + font-size: 1.75rem; + color: var(--color-dark-grey-400); + line-height: 1.5; + margin-top: 0.5rem; + } + + &__progress-tracker-section { + flex: 1 1 600px; + min-width: 0; + display: flex; + align-items: center; + justify-content: center; } } -// === Expandable Details === -.order-details { - margin: 1rem; - padding: 1rem; - background-color: rgb(248 250 252 / 50%); - border-radius: 0.5rem; - border: 1px solid rgb(243 244 246 / 60%); +.legend-marketplace-progress-tracker { + width: 100%; + padding: 2rem 0; - &__title { - font-size: 2rem !important; - font-weight: 600 !important; - margin-bottom: 0.75rem !important; + .MuiStepper-root { + padding: 0; + } + + &__step-label { + font-size: 1.8rem; + font-weight: 600; color: var(--color-dark-grey-100); + margin-top: 0.5rem; } - &__items-table { - .MuiTable-root { - .MuiTableHead-root { - background: linear-gradient( - 135deg, - var(--color-light-grey-0), - var(--color-light-grey-50) - ); - - .MuiTableCell-root { - border-bottom: 2px solid var(--color-blue-200) !important; - padding: 12px 10px !important; - font-size: 1.5rem !important; - font-weight: 700 !important; - color: var(--color-dark-grey-400) !important; - text-transform: uppercase; - letter-spacing: 0.075em; - position: relative; - - &::after { - content: ''; - position: absolute; - bottom: -2px; - left: 10px; - right: 10px; - height: 1px; - background: linear-gradient( - 90deg, - transparent, - var(--color-blue-100), - transparent - ); - } - - .MuiTypography-root { - font-size: 1.6rem !important; - font-weight: 700 !important; - color: var(--color-dark-grey-500) !important; - text-shadow: 0 1px 2px rgb(0 0 0 / 5%); - } - } - } + &__step-details { + margin-top: 0.75rem; + padding: 0.75rem; + border-radius: 0.375rem; + } - .MuiTableBody-root { - .MuiTableRow-root { - cursor: default; - transition: all 0.2s ease; - - &:hover { - background-color: var(--color-light-shade-50) !important; - transform: translateY(-1px); - } - - &:nth-child(odd) { - background-color: rgba(var(--color-light-grey-0), 0.3); - } - - .MuiTableCell-root { - padding: 0.5rem 0.375rem !important; - border-bottom: 1px solid rgb(243 244 246 / 30%) !important; - font-size: 1.5rem; - font-weight: 500; - color: var(--color-dark-grey-400); - vertical-align: middle; - } - } - } + &__step-detail { + font-size: 1rem; + color: var(--color-dark-grey-400); + margin-bottom: 0.25rem; + line-height: 1.5; + + &:last-child { + margin-bottom: 0; + } + + strong { + font-weight: 600; + color: var(--color-dark-grey-100); } } - &__product-name { - font-weight: 600 !important; - color: var(--color-dark-grey-400); - font-size: 1.3rem !important; - line-height: 1.2; + &__step-icon { + &--completed { + color: var(--color-blue-100); + font-size: 2.3rem; + } + + &--active { + color: var(--color-blue-100); + font-size: 2.3rem; + } + + &--pending { + color: var(--color-light-grey-400); + font-size: 2.3rem; + } + + &--rejected { + color: var(--color-red-100); + font-size: 2.3rem; + } } +} - &__provider-name { - font-weight: 600 !important; - color: var(--color-dark-grey-300); - font-size: 1.3rem !important; - line-height: 1.2; +.legend-marketplace-cancel-order-dialog { + .MuiPaper-root { + border-radius: 1rem; + padding: 0.5rem; } - &__category-name { - font-weight: 600 !important; - color: var(--color-dark-grey-300); - font-size: 1.3rem !important; - text-transform: uppercase !important; - line-height: 1.2 !important; + &__title { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 2rem; + + .MuiTypography-root { + font-size: 2rem; + font-weight: 600; + } } - &__provider-chip { - background-color: var(--color-white) !important; - border: 1px solid var(--color-light-grey-200) !important; - color: var(--color-dark-grey-400) !important; - font-size: 1.5rem !important; - height: 20px !important; - font-weight: 500 !important; - border-radius: 0.375rem !important; + &__warning-icon { + color: var(--color-yellow-400); + font-size: 2.5rem; } - &__price { + &__content { + padding: 1rem 0; font-size: 1.5rem; - font-weight: 500 !important; - color: var(--color-dark-grey-400); + } + + &__message { + font-size: 1.8rem; + color: var(--color-black); + margin-bottom: 1.5rem; + } + + &__order-info { + padding: 1rem; + background-color: var(--color-light-grey-50); + border-radius: 0.5rem; + border: 1px solid var(--color-light-grey-200); + margin-bottom: 1.5rem; + + .MuiTypography-root { + font-size: 1.5rem; + color: var(--color-dark-grey-400); + margin-bottom: 0.5rem; + + &:last-child { + margin-bottom: 0; + } + + strong { + font-weight: 600; + color: var(--color-dark-grey-100); + } + } + } + + &__text-field { + .MuiInputLabel-root { + font-weight: 500; + font-size: 1.5rem; + } + + .MuiOutlinedInput-root { + border-radius: 0.5rem; + font-size: 1.5rem; + + &:hover .MuiOutlinedInput-notchedOutline { + border-color: var(--color-blue-100); + } + + &.Mui-focused .MuiOutlinedInput-notchedOutline { + border-color: var(--color-blue-100); + border-width: 2px; + } + } + + .MuiFormHelperText-root { + font-size: 1.25rem; + margin-top: 0.5rem; + } + } + + &__actions { + padding: 1rem 1.5rem; + gap: 0.75rem; + + .MuiButton-root { + font-weight: 500; + text-transform: none; + border-radius: 0.5rem; + padding: 0.5rem 1.25rem; + font-size: 1.5rem; + @include transition-smooth; + + &:hover { + transform: translateY(-1px); + } + } } } diff --git a/packages/legend-art/style/base/_variables.scss b/packages/legend-art/style/base/_variables.scss index 593b2ae282b..8962082366e 100644 --- a/packages/legend-art/style/base/_variables.scss +++ b/packages/legend-art/style/base/_variables.scss @@ -58,6 +58,7 @@ --color-pink-380: #c5375f; --color-pink-400: #b33659; --color-pink-500: #af2a5b; + --color-red-0: #ffe6e3; --color-red-50: #ff958c; --color-red-100: #f5584b; --color-red-150: #f95252; @@ -219,6 +220,16 @@ --color-modal-category-bg: #99f6e4; --color-modal-category-text: #0d9488; + // Orders page colors + --color-order-chip-terminal-bg: #e1e6fd; + --color-order-chip-terminal-text: #0019f7; + --color-order-chip-addon-bg: #d0e8f7; + --color-order-chip-addon-text: #015383; + --color-order-chip-category-bg: #ebe5f4; + --color-order-chip-category-text: #3d0098; + --color-order-chip-price-bg: #dcfce7; + --color-order-chip-price-text: #166534; + // font --font-entity-label: 'Roboto Condensed'; } diff --git a/packages/legend-server-marketplace/src/MarketplaceServerClient.ts b/packages/legend-server-marketplace/src/MarketplaceServerClient.ts index f93a14c2985..94510d1b28c 100644 --- a/packages/legend-server-marketplace/src/MarketplaceServerClient.ts +++ b/packages/legend-server-marketplace/src/MarketplaceServerClient.ts @@ -15,7 +15,11 @@ */ import { type PlainObject, AbstractServerClient } from '@finos/legend-shared'; -import type { LightProvider, TerminalResult } from './models/Provider.js'; +import type { + LightProvider, + TerminalServicesResponse, + ProductType, +} from './models/Provider.js'; import type { DataProductSearchResult } from './models/DataProductSearchResult.js'; import type { SubscriptionRequest, @@ -33,19 +37,13 @@ import { type TerminalProductOrderResponse, OrderStatusCategory, } from './models/TerminalProductOrder.js'; +import type { FetchProductsParams } from '@finos/legend-application-marketplace'; export interface MarketplaceServerClientConfig { serverUrl: string; subscriptionUrl: string; } -interface TerminalAddOnServerResponse { - hrid: string; - desktops?: T; - addons?: T; - landing_addons?: T; -} - interface MarketplaceServerResponse { response_code: string; status: string; @@ -75,33 +73,36 @@ export class MarketplaceServerClient extends AbstractServerClient { getVendors = (): Promise[]> => this.get(this._vendors()); - getVendorsByCategory = async ( - user: string, - category: string, - viewType: string, - filters: string, - limit: number, - ): Promise[]> => { - const response = await this.get< - TerminalAddOnServerResponse[]> - >(`${this.baseUrl}/v1/service/${viewType}/${category}?kerberos=${user}`); - let result = []; - if (viewType === 'landing') { - result = - category === 'desktop' - ? (response.desktops ?? []) - : (response.landing_addons ?? []); - } else { - result = - category === 'desktop' - ? (response.desktops ?? []) - : (response.addons ?? []); + private _products = (): string => `${this.baseUrl}/v1/workflow/products`; + + fetchProducts = async ( + params: FetchProductsParams, + ): Promise> => { + const queryParams: Record = { + kerberos: params.kerberos, + product_type: params.product_type, + preferred_products: params.preferred_products, + }; + + if (params.page_number !== undefined) { + queryParams.page_number = params.page_number; + } + if (params.page_size !== undefined) { + queryParams.page_size = params.page_size; + } + if (params.search !== undefined && params.search !== '') { + queryParams.search = params.search; } - return result; + return this.get>( + this._products(), + undefined, + undefined, + queryParams, + ); }; - // ------------------------------------------- Search- ------------------------------------------- + // ------------------------------------------- Search ------------------------------------------- private _search = (): string => `${this.baseUrl}/v1/search`; @@ -182,4 +183,12 @@ export class MarketplaceServerClient extends AbstractServerClient { category, }, ); + + cancelOrder = async (cancelData: { + order_id: string; + kerberos: string; + comments: string; + process_instance_id: string; + }): Promise> => + this.post(`${this.baseUrl}/v1/workflow/cancel/order`, cancelData); } diff --git a/packages/legend-server-marketplace/src/models/Cart.ts b/packages/legend-server-marketplace/src/models/Cart.ts index 58acfbdfede..a6f1fe37a85 100644 --- a/packages/legend-server-marketplace/src/models/Cart.ts +++ b/packages/legend-server-marketplace/src/models/Cart.ts @@ -25,7 +25,8 @@ export interface CartItemRequest { model?: string; description: string; isOwned: string; - vendorProfileId?: number; + skipWorkflow?: boolean; + cartId?: number; } export interface CartItem extends CartItemRequest { diff --git a/packages/legend-server-marketplace/src/models/Provider.ts b/packages/legend-server-marketplace/src/models/Provider.ts index 28617bf2376..b02b04b0e52 100644 --- a/packages/legend-server-marketplace/src/models/Provider.ts +++ b/packages/legend-server-marketplace/src/models/Provider.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { SerializationFactory } from '@finos/legend-shared'; -import { createModelSchema, primitive } from 'serializr'; +import { SerializationFactory, type PlainObject } from '@finos/legend-shared'; +import { createModelSchema, optional, primitive } from 'serializr'; export interface LightProvider { description: string; @@ -23,6 +23,12 @@ export interface LightProvider { type: string; } +export enum ProductType { + ALL = 'ALL', + VENDOR_PROFILE = 'VENDOR_PROFILE', + SERVICE_PRICING = 'SERVICE_PRICING', +} + export enum TerminalItemType { TERMINAL = 'Terminal', ADD_ON = 'Add-On', @@ -36,6 +42,8 @@ export class TerminalResult { description!: string; price!: number; phystr!: string; + model!: string | null; + skipWorkflow?: boolean; isOwned?: boolean; vendorProfileId?: number; @@ -48,6 +56,8 @@ export class TerminalResult { description: primitive(), price: primitive(), phystr: primitive(), + model: primitive(), + skipWorkflow: optional(primitive()), isOwned: primitive(), vendorProfileId: primitive(), }), @@ -64,3 +74,12 @@ export interface Filter { label: string; value: string; } + +export interface TerminalServicesResponse { + hrid: string; + vendor_profiles?: PlainObject[]; + service_pricing?: PlainObject[]; + vendor_profiles_total_count?: number; + service_pricing_total_count?: number; + total_count?: number; +} diff --git a/packages/legend-server-marketplace/src/models/TerminalProductOrder.ts b/packages/legend-server-marketplace/src/models/TerminalProductOrder.ts index 4ed017ea862..5f83ebf4001 100644 --- a/packages/legend-server-marketplace/src/models/TerminalProductOrder.ts +++ b/packages/legend-server-marketplace/src/models/TerminalProductOrder.ts @@ -49,13 +49,28 @@ export interface TerminalProductOrder { } export interface ServicePricingItems { - service_pricing_id: number; - service_pricing_name: string; + entity_id: number; + entity_name: string; + entity_category: string; + entity_type: string; + entity_cost: number; } export interface WorkflowDetails { - current_stage: string; + manager_process_id: string | null; + manager_actioned_by: string | null; + manager_actioned_timestamp: string | null; + manager_comment: string | null; + manager_action: string | null; + bbg_approval_process_id: string | null; + bbg_approval_actioned_by: string | null; + bbg_approval_actioned_timestamp: string | null; + bbg_approval_comment: string | null; + bbg_approval_action: string | null; + rpm_ticket_id: string | null; + current_stage: string | null; workflow_status: OrderStatus; + rpm_action: string | null; } export interface TerminalProductOrderResponse {