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 { formatApiPath } from 'utils/formatPath';
|
||||||
import { useDetailedInvoices } from 'hooks/api/getters/useDetailedInvoices/useDetailedInvoices';
|
import { useDetailedInvoices } from 'hooks/api/getters/useDetailedInvoices/useDetailedInvoices';
|
||||||
import { formatCurrency } from '../BillingInvoices/BillingInvoice/formatCurrency.js';
|
import { formatCurrency } from '../BillingInvoices/BillingInvoice/formatCurrency.js';
|
||||||
|
import { BillingInfoSkeleton } from './BillingInfoSkeleton.tsx';
|
||||||
const PORTAL_URL = formatApiPath('api/admin/invoices');
|
const PORTAL_URL = formatApiPath('api/admin/invoices');
|
||||||
|
|
||||||
type BillingInfoProps = {};
|
type BillingInfoProps = {};
|
||||||
@ -61,11 +62,20 @@ const GetInTouch: FC = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const BillingInfo: FC<BillingInfoProps> = () => {
|
export const BillingInfo: FC<BillingInfoProps> = () => {
|
||||||
const { instanceStatus } = useInstanceStatus();
|
const { instanceStatus, loading: instanceStatusLoading } =
|
||||||
|
useInstanceStatus();
|
||||||
const {
|
const {
|
||||||
uiConfig: { billing },
|
uiConfig: { billing },
|
||||||
} = useUiConfig();
|
} = useUiConfig();
|
||||||
const { planPrice, planCurrency } = useDetailedInvoices();
|
const {
|
||||||
|
planPrice,
|
||||||
|
planCurrency,
|
||||||
|
loading: invoicesLoading,
|
||||||
|
} = useDetailedInvoices();
|
||||||
|
|
||||||
|
if (instanceStatusLoading || invoicesLoading) {
|
||||||
|
return <BillingInfoSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
if (!instanceStatus) {
|
if (!instanceStatus) {
|
||||||
return (
|
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 { Box, styled } from '@mui/material';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { BillingInvoice } from './BillingInvoice/BillingInvoice.tsx';
|
import { BillingInvoice } from './BillingInvoice/BillingInvoice.tsx';
|
||||||
|
import { BillingInvoiceSkeleton } from './BillingInvoice/BillingInvoiceSkeleton.tsx';
|
||||||
import { useDetailedInvoices } from 'hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts';
|
import { useDetailedInvoices } from 'hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts';
|
||||||
import { TablePlaceholder } from 'component/common/Table';
|
import { TablePlaceholder } from 'component/common/Table';
|
||||||
|
import useLoading from 'hooks/useLoading';
|
||||||
|
|
||||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -12,9 +14,16 @@ const StyledContainer = styled(Box)(({ theme }) => ({
|
|||||||
|
|
||||||
export const BillingInvoices: FC = () => {
|
export const BillingInvoices: FC = () => {
|
||||||
const { invoices, loading } = useDetailedInvoices();
|
const { invoices, loading } = useDetailedInvoices();
|
||||||
|
const ref = useLoading(loading);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <StyledContainer />;
|
return (
|
||||||
|
<StyledContainer ref={ref}>
|
||||||
|
{[1, 2, 3].map((index) => (
|
||||||
|
<BillingInvoiceSkeleton key={index} />
|
||||||
|
))}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user