Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/components/tables/Billing/Rows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,10 @@ function Row({ row, isSelected }: RowProps) {
);
}

// TODO (billing): Remove pagination placeholder when the new RPC is available.
function Rows({ data, selectedInvoice }: RowsProps) {
return (
<>
{data.slice(0, 4).map((record, index) => (
{data.map((record, index) => (
<Row
row={record}
key={index}
Expand Down
135 changes: 81 additions & 54 deletions src/components/tables/Billing/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { TableColumns } from 'src/types';

import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';

import { Box, Table, TableContainer } from '@mui/material';
import { Box, Table, TableContainer, TablePagination } from '@mui/material';

import { useIntl } from 'react-intl';

import Rows from 'src/components/tables/Billing/Rows';
import EntityTableBody from 'src/components/tables/EntityTable/TableBody';
import EntityTableHeader from 'src/components/tables/EntityTable/TableHeader';
import TablePaginationActions from 'src/components/tables/PaginationActions';
import { getTableHeaderWithoutHeaderColor } from 'src/context/Theme';
import { useBillingInvoices } from 'src/hooks/billing/useBillingInvoices';
import { TableStatuses } from 'src/types';
Expand Down Expand Up @@ -38,65 +39,91 @@ export const columns: TableColumns[] = [
},
];

// TODO (billing): Use the getStatsForBillingHistoryTable query function as the primary source of data for this view
// when a database table containing historic billing data is available.
const ROWS_PER_PAGE = 4;

function BillingHistoryTable() {
const intl = useIntl();

const {
invoices: billingHistory,
selectedInvoice,
isLoading,
networkFailed,
} = useBillingInvoices();

const dataRows = useMemo(
() =>
billingHistory.length > 0 ? (
<Rows
data={billingHistory}
selectedInvoice={
selectedInvoice ? invoiceId(selectedInvoice) : null
}
/>
) : null,
[billingHistory, selectedInvoice]
);
const { allInvoices, selectedInvoice, isLoading, networkFailed } =
useBillingInvoices();

const [page, setPage] = useState(0);

// The selected tenant lives behind the hook; reset to the first (newest)
// page whenever the invoice set changes so a tenant switch starts at the
// top rather than stranding the view on a now-out-of-range page.
useEffect(() => {
setPage(0);
}, [allInvoices]);

const pageCount = Math.ceil(allInvoices.length / ROWS_PER_PAGE);
const currentPage = Math.min(page, Math.max(0, pageCount - 1));

const dataRows = useMemo(() => {
if (allInvoices.length === 0) {
return null;
}

const start = currentPage * ROWS_PER_PAGE;

return (
<Rows
data={allInvoices.slice(start, start + ROWS_PER_PAGE)}
selectedInvoice={
selectedInvoice ? invoiceId(selectedInvoice) : null
}
/>
);
}, [allInvoices, currentPage, selectedInvoice]);

return (
<TableContainer component={Box}>
<Table
aria-label={intl.formatMessage({
id: 'entityTable.title',
})}
size="small"
sx={{
...getTableHeaderWithoutHeaderColor(),
minWidth: 450,
}}
>
<EntityTableHeader columns={columns} />

<EntityTableBody
columns={columns}
noExistingDataContentIds={{
header: 'admin.billing.table.history.emptyTableDefault.header',
message:
'admin.billing.table.history.emptyTableDefault.message',
disableDoclink: true,
<Box>
<TableContainer component={Box}>
<Table
aria-label={intl.formatMessage({
id: 'entityTable.title',
})}
size="small"
sx={{
...getTableHeaderWithoutHeaderColor(),
minWidth: 450,
}}
tableState={
billingHistory.length > 0
? { status: TableStatuses.DATA_FETCHED }
: networkFailed
? { status: TableStatuses.NETWORK_FAILED }
: { status: TableStatuses.NO_EXISTING_DATA }
}
loading={isLoading}
rows={dataRows}
>
<EntityTableHeader columns={columns} />

<EntityTableBody
columns={columns}
noExistingDataContentIds={{
header: 'admin.billing.table.history.emptyTableDefault.header',
message:
'admin.billing.table.history.emptyTableDefault.message',
disableDoclink: true,
}}
tableState={
allInvoices.length > 0
? { status: TableStatuses.DATA_FETCHED }
: networkFailed
? { status: TableStatuses.NETWORK_FAILED }
: { status: TableStatuses.NO_EXISTING_DATA }
}
loading={isLoading}
rows={dataRows}
/>
</Table>
</TableContainer>

{allInvoices.length > ROWS_PER_PAGE ? (
<TablePagination
component="div"
count={allInvoices.length}
page={currentPage}
rowsPerPage={ROWS_PER_PAGE}
rowsPerPageOptions={[]}
onPageChange={(_event, newPage) => setPage(newPage)}
ActionsComponent={TablePaginationActions}
/>
</Table>
</TableContainer>
) : null}
</Box>
);
}

Expand Down
24 changes: 17 additions & 7 deletions src/hooks/billing/useBillingInvoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import { useTenantStore } from 'src/stores/Tenant';
import { invoiceId, stripTimeFromDate } from 'src/utils/billing-utils';

export interface UseBillingInvoicesResult {
// The rolling-window subset (plus manual invoices) used by the usage
// graphs, which only chart the recent period.
invoices: Invoice[];
// Every invoice for the tenant, newest first. Drives the full, paginated
// history table.
allInvoices: Invoice[];
// The invoice currently shown in the line-item/detail views: the stored
// selection if it still exists in this tenant's data, otherwise the newest
// invoice. Falling back this way means an org switch self-corrects without
Expand Down Expand Up @@ -111,34 +116,39 @@ export function useBillingInvoices(): UseBillingInvoicesResult {
pause: !selectedTenant,
});

const invoices = useMemo(() => {
const allInvoices = useMemo(() => {
const nodes = data?.tenant?.billing.invoices.nodes ?? [];

return nodes
.map((node) => mapInvoice(node, selectedTenant))
.filter((invoice) => isVisible(invoice, dateWindow))
.sort((a, b) =>
compareDesc(
stripTimeFromDate(a.date_start),
stripTimeFromDate(b.date_start)
)
);
}, [data, dateWindow, selectedTenant]);
}, [data, selectedTenant]);

const invoices = useMemo(
() => allInvoices.filter((invoice) => isVisible(invoice, dateWindow)),
[allInvoices, dateWindow]
);

const selectedInvoice = useMemo(() => {
if (invoices.length === 0) {
if (allInvoices.length === 0) {
return null;
}

return (
invoices.find(
allInvoices.find(
(invoice) => invoiceId(invoice) === selectedInvoiceId
) ?? invoices[0]
) ?? allInvoices[0]
);
}, [invoices, selectedInvoiceId]);
}, [allInvoices, selectedInvoiceId]);

return {
invoices,
allInvoices,
selectedInvoice,
isLoading: fetching,
networkFailed: Boolean(error?.networkError),
Expand Down