mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: add billing invoices skeletons (#10827)
Since we load directly from Stripe, it takes a little time. Good to have skeletons. <img width="2151" height="1587" alt="Screenshot from 2025-10-17 15-32-10" src="https://github.com/user-attachments/assets/be767ea1-b95f-4ef3-abf6-e8302e7092fd" />
This commit is contained in:
		
							parent
							
								
									faad097915
								
							
						
					
					
						commit
						c8ca11aebb
					
				@ -7,6 +7,7 @@ import { InstanceState } from 'interfaces/instance';
 | 
			
		||||
import { formatApiPath } from 'utils/formatPath';
 | 
			
		||||
import { useDetailedInvoices } from 'hooks/api/getters/useDetailedInvoices/useDetailedInvoices';
 | 
			
		||||
import { formatCurrency } from '../BillingInvoices/BillingInvoice/formatCurrency.js';
 | 
			
		||||
import { BillingInfoSkeleton } from './BillingInfoSkeleton.tsx';
 | 
			
		||||
const PORTAL_URL = formatApiPath('api/admin/invoices');
 | 
			
		||||
 | 
			
		||||
type BillingInfoProps = {};
 | 
			
		||||
@ -61,11 +62,20 @@ const GetInTouch: FC = () => (
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const BillingInfo: FC<BillingInfoProps> = () => {
 | 
			
		||||
    const { instanceStatus } = useInstanceStatus();
 | 
			
		||||
    const { instanceStatus, loading: instanceStatusLoading } =
 | 
			
		||||
        useInstanceStatus();
 | 
			
		||||
    const {
 | 
			
		||||
        uiConfig: { billing },
 | 
			
		||||
    } = useUiConfig();
 | 
			
		||||
    const { planPrice, planCurrency } = useDetailedInvoices();
 | 
			
		||||
    const {
 | 
			
		||||
        planPrice,
 | 
			
		||||
        planCurrency,
 | 
			
		||||
        loading: invoicesLoading,
 | 
			
		||||
    } = useDetailedInvoices();
 | 
			
		||||
 | 
			
		||||
    if (instanceStatusLoading || invoicesLoading) {
 | 
			
		||||
        return <BillingInfoSkeleton />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!instanceStatus) {
 | 
			
		||||
        return (
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,74 @@
 | 
			
		||||
import { Paper, styled } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
const StyledWrapper = styled(Paper)(({ theme }) => ({
 | 
			
		||||
    padding: theme.spacing(2),
 | 
			
		||||
    borderRadius: theme.shape.borderRadiusLarge,
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledRow = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    justifyContent: 'space-between',
 | 
			
		||||
    fontSize: theme.typography.body2.fontSize,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const SkeletonBox = styled('div')(({ theme }) => ({
 | 
			
		||||
    height: theme.spacing(2.5),
 | 
			
		||||
    backgroundColor: theme.palette.action.hover,
 | 
			
		||||
    borderRadius: theme.spacing(0.5),
 | 
			
		||||
    margin: theme.spacing(0.5, 0),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const SkeletonTitle = styled('div')(({ theme }) => ({
 | 
			
		||||
    height: theme.spacing(4),
 | 
			
		||||
    backgroundColor: theme.palette.action.hover,
 | 
			
		||||
    borderRadius: theme.spacing(0.5),
 | 
			
		||||
    margin: theme.spacing(0.5, 0),
 | 
			
		||||
    width: theme.spacing(20),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const SkeletonButton = styled('div')(({ theme }) => ({
 | 
			
		||||
    height: theme.spacing(5),
 | 
			
		||||
    backgroundColor: theme.palette.action.hover,
 | 
			
		||||
    borderRadius: theme.spacing(0.5),
 | 
			
		||||
    margin: theme.spacing(1, 0),
 | 
			
		||||
    width: '100%',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const SkeletonDivider = styled('div')(({ theme }) => ({
 | 
			
		||||
    height: theme.spacing(0.125),
 | 
			
		||||
    backgroundColor: theme.palette.action.hover,
 | 
			
		||||
    borderRadius: theme.spacing(0.5),
 | 
			
		||||
    margin: theme.spacing(1.5, 0),
 | 
			
		||||
    width: '100%',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const SkeletonText = styled('div')(({ theme }) => ({
 | 
			
		||||
    height: theme.spacing(2),
 | 
			
		||||
    backgroundColor: theme.palette.action.hover,
 | 
			
		||||
    borderRadius: theme.spacing(0.5),
 | 
			
		||||
    margin: theme.spacing(0.5, 0),
 | 
			
		||||
    width: theme.spacing(25),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const BillingInfoSkeleton = () => {
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledWrapper data-loading>
 | 
			
		||||
            <SkeletonTitle />
 | 
			
		||||
            <StyledRow>
 | 
			
		||||
                <SkeletonBox sx={{ width: (theme) => theme.spacing(12) }} />
 | 
			
		||||
                <SkeletonBox sx={{ width: (theme) => theme.spacing(8) }} />
 | 
			
		||||
            </StyledRow>
 | 
			
		||||
            <StyledRow>
 | 
			
		||||
                <SkeletonBox sx={{ width: (theme) => theme.spacing(10) }} />
 | 
			
		||||
                <SkeletonBox sx={{ width: (theme) => theme.spacing(6) }} />
 | 
			
		||||
            </StyledRow>
 | 
			
		||||
            <SkeletonDivider />
 | 
			
		||||
            <SkeletonButton />
 | 
			
		||||
            <SkeletonText />
 | 
			
		||||
            <SkeletonText sx={{ width: (theme) => theme.spacing(20) }} />
 | 
			
		||||
        </StyledWrapper>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,83 @@
 | 
			
		||||
import {
 | 
			
		||||
    styled,
 | 
			
		||||
    Accordion,
 | 
			
		||||
    AccordionSummary,
 | 
			
		||||
    AccordionDetails,
 | 
			
		||||
} from '@mui/material';
 | 
			
		||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
 | 
			
		||||
 | 
			
		||||
const StyledAccordion = styled(Accordion)(({ theme }) => ({
 | 
			
		||||
    background: theme.palette.background.paper,
 | 
			
		||||
    borderRadius: theme.shape.borderRadiusLarge,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const HeaderRoot = styled(AccordionSummary)(({ theme }) => ({
 | 
			
		||||
    padding: theme.spacing(2, 4),
 | 
			
		||||
    gap: theme.spacing(1.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const HeaderLeft = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    gap: theme.spacing(1.5),
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    minWidth: 0,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const HeaderRight = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const SkeletonBox = styled('div')(({ theme }) => ({
 | 
			
		||||
    height: theme.spacing(2.5),
 | 
			
		||||
    backgroundColor: theme.palette.action.hover,
 | 
			
		||||
    borderRadius: theme.spacing(0.5),
 | 
			
		||||
    margin: theme.spacing(0.5, 0),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const SkeletonBadge = styled('div')(({ theme }) => ({
 | 
			
		||||
    height: theme.spacing(3),
 | 
			
		||||
    width: theme.spacing(10),
 | 
			
		||||
    backgroundColor: theme.palette.action.hover,
 | 
			
		||||
    borderRadius: theme.spacing(1.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const SkeletonAmount = styled('div')(({ theme }) => ({
 | 
			
		||||
    height: theme.spacing(3),
 | 
			
		||||
    width: theme.spacing(12.5),
 | 
			
		||||
    backgroundColor: theme.palette.action.hover,
 | 
			
		||||
    borderRadius: theme.spacing(0.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const SkeletonContent = styled('div')(({ theme }) => ({
 | 
			
		||||
    height: theme.spacing(25),
 | 
			
		||||
    backgroundColor: theme.palette.action.hover,
 | 
			
		||||
    borderRadius: theme.spacing(0.5),
 | 
			
		||||
    margin: theme.spacing(2, 4),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const BillingInvoiceSkeleton = () => {
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledAccordion defaultExpanded data-loading>
 | 
			
		||||
            <HeaderRoot expandIcon={<ExpandMoreIcon />}>
 | 
			
		||||
                <HeaderLeft>
 | 
			
		||||
                    <SkeletonBox
 | 
			
		||||
                        sx={{
 | 
			
		||||
                            width: (theme) => theme.spacing(15),
 | 
			
		||||
                            height: (theme) => theme.spacing(4),
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                </HeaderLeft>
 | 
			
		||||
                <HeaderRight>
 | 
			
		||||
                    <SkeletonBadge />
 | 
			
		||||
                    <SkeletonAmount />
 | 
			
		||||
                </HeaderRight>
 | 
			
		||||
            </HeaderRoot>
 | 
			
		||||
            <AccordionDetails>
 | 
			
		||||
                <SkeletonContent />
 | 
			
		||||
            </AccordionDetails>
 | 
			
		||||
        </StyledAccordion>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
import { Box, styled } from '@mui/material';
 | 
			
		||||
import type { FC } from 'react';
 | 
			
		||||
import { BillingInvoice } from './BillingInvoice/BillingInvoice.tsx';
 | 
			
		||||
import { BillingInvoiceSkeleton } from './BillingInvoice/BillingInvoiceSkeleton.tsx';
 | 
			
		||||
import { useDetailedInvoices } from 'hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts';
 | 
			
		||||
import { TablePlaceholder } from 'component/common/Table';
 | 
			
		||||
import useLoading from 'hooks/useLoading';
 | 
			
		||||
 | 
			
		||||
const StyledContainer = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
@ -12,9 +14,16 @@ const StyledContainer = styled(Box)(({ theme }) => ({
 | 
			
		||||
 | 
			
		||||
export const BillingInvoices: FC = () => {
 | 
			
		||||
    const { invoices, loading } = useDetailedInvoices();
 | 
			
		||||
    const ref = useLoading(loading);
 | 
			
		||||
 | 
			
		||||
    if (loading) {
 | 
			
		||||
        return <StyledContainer />;
 | 
			
		||||
        return (
 | 
			
		||||
            <StyledContainer ref={ref}>
 | 
			
		||||
                {[1, 2, 3].map((index) => (
 | 
			
		||||
                    <BillingInvoiceSkeleton key={index} />
 | 
			
		||||
                ))}
 | 
			
		||||
            </StyledContainer>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user