1
0
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:
Jaanus Sellin 2025-10-17 15:40:36 +03:00 committed by GitHub
parent faad097915
commit c8ca11aebb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 179 additions and 3 deletions

View File

@ -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 (

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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 (