mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
chore: refactor pagination (#4753)
* Extract PageButton * Fix import * Extract utils * Format * Separate pagination - wip * Spawn pagination machine - buggy filter * Make labels optional * Layout, fix send reset bug * Format * Fix refresh data bug * Remove debugging line * Fix url updates setSearchParams overwrites all search params, rather than merging * Update Audit Page * Simplify pagination widget * Fix workspaces story * Fix Audit story * Fix pagination story and pagebutton highlight * Fix pagination tests * Add to utils tests * Format * Add tests
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
|
||||
interface PageButtonProps {
|
||||
activePage?: number
|
||||
page?: number
|
||||
placeholder?: string
|
||||
numPages?: number
|
||||
onPageClick?: (page: number) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const PageButton = ({
|
||||
activePage,
|
||||
page,
|
||||
placeholder = "...",
|
||||
numPages,
|
||||
onPageClick,
|
||||
disabled = false,
|
||||
}: PageButtonProps): JSX.Element => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<Button
|
||||
className={
|
||||
activePage === page
|
||||
? `${styles.pageButton} ${styles.activePageButton}`
|
||||
: styles.pageButton
|
||||
}
|
||||
aria-label={`${page === activePage ? "Current Page" : ""} ${
|
||||
page === numPages ? "Last Page" : ""
|
||||
} Page${page}`}
|
||||
name={page === undefined ? undefined : "Page button"}
|
||||
onClick={() => onPageClick && page && onPageClick(page)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div>{page ?? placeholder}</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
pageButton: {
|
||||
"&:not(:last-of-type)": {
|
||||
marginRight: theme.spacing(0.5),
|
||||
},
|
||||
},
|
||||
|
||||
activePageButton: {
|
||||
borderColor: `${theme.palette.info.main}`,
|
||||
backgroundColor: `${theme.palette.info.dark}`,
|
||||
},
|
||||
}))
|
||||
@@ -1,65 +1,53 @@
|
||||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import { PaginationWidget, PaginationWidgetProps } from "./PaginationWidget"
|
||||
import { createPaginationRef } from "./utils"
|
||||
|
||||
export default {
|
||||
title: "components/PaginationWidget",
|
||||
component: PaginationWidget,
|
||||
argTypes: {
|
||||
prevLabel: {
|
||||
defaultValue: "Previous",
|
||||
},
|
||||
nextLabel: {
|
||||
defaultValue: "Next",
|
||||
},
|
||||
paginationRef: {
|
||||
defaultValue: createPaginationRef({ page: 1, limit: 12 }),
|
||||
},
|
||||
numRecords: {
|
||||
defaultValue: 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const Template: Story<PaginationWidgetProps> = (
|
||||
args: PaginationWidgetProps,
|
||||
) => <PaginationWidget {...args} />
|
||||
|
||||
const defaultProps = {
|
||||
prevLabel: "Previous",
|
||||
nextLabel: "Next",
|
||||
onPrevClick: action("previous"),
|
||||
onNextClick: action("next"),
|
||||
onPageClick: action("clicked"),
|
||||
}
|
||||
|
||||
export const UnknownPageNumbers = Template.bind({})
|
||||
UnknownPageNumbers.args = {
|
||||
...defaultProps,
|
||||
numRecords: undefined,
|
||||
}
|
||||
|
||||
export const LessThan8Pages = Template.bind({})
|
||||
LessThan8Pages.args = {
|
||||
...defaultProps,
|
||||
numRecords: 84,
|
||||
numRecordsPerPage: 12,
|
||||
activePage: 1,
|
||||
}
|
||||
|
||||
export const MoreThan8Pages = Template.bind({})
|
||||
MoreThan8Pages.args = {
|
||||
...defaultProps,
|
||||
numRecords: 200,
|
||||
numRecordsPerPage: 12,
|
||||
activePage: 1,
|
||||
}
|
||||
|
||||
export const MoreThan7PagesWithActivePageCloseToStart = Template.bind({})
|
||||
MoreThan7PagesWithActivePageCloseToStart.args = {
|
||||
...defaultProps,
|
||||
numRecords: 200,
|
||||
numRecordsPerPage: 12,
|
||||
activePage: 2,
|
||||
paginationRef: createPaginationRef({ page: 2, limit: 12 }),
|
||||
}
|
||||
|
||||
export const MoreThan7PagesWithActivePageFarFromBoundaries = Template.bind({})
|
||||
MoreThan7PagesWithActivePageFarFromBoundaries.args = {
|
||||
...defaultProps,
|
||||
numRecords: 200,
|
||||
numRecordsPerPage: 12,
|
||||
activePage: 4,
|
||||
paginationRef: createPaginationRef({ page: 4, limit: 12 }),
|
||||
}
|
||||
|
||||
export const MoreThan7PagesWithActivePageCloseToEnd = Template.bind({})
|
||||
MoreThan7PagesWithActivePageCloseToEnd.args = {
|
||||
...defaultProps,
|
||||
numRecords: 200,
|
||||
numRecordsPerPage: 12,
|
||||
activePage: 17,
|
||||
paginationRef: createPaginationRef({ page: 17, limit: 12 }),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { screen } from "@testing-library/react"
|
||||
import { render } from "../../testHelpers/renderHelpers"
|
||||
import { PaginationWidget } from "./PaginationWidget"
|
||||
import { createPaginationRef } from "./utils"
|
||||
|
||||
describe("PaginatedList", () => {
|
||||
it("displays an accessible previous and next button", () => {
|
||||
@@ -8,20 +9,13 @@ describe("PaginatedList", () => {
|
||||
<PaginationWidget
|
||||
prevLabel="Previous"
|
||||
nextLabel="Next"
|
||||
paginationRef={createPaginationRef({ page: 2, limit: 12 })}
|
||||
numRecords={200}
|
||||
numRecordsPerPage={12}
|
||||
activePage={1}
|
||||
onPrevClick={() => jest.fn()}
|
||||
onNextClick={() => jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Previous page" }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Next page" }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole("button", { name: "Previous page" })).toBeEnabled()
|
||||
expect(screen.getByRole("button", { name: "Next page" })).toBeEnabled()
|
||||
})
|
||||
|
||||
it("displays the expected number of pages with one ellipsis tile", () => {
|
||||
@@ -29,12 +23,8 @@ describe("PaginatedList", () => {
|
||||
<PaginationWidget
|
||||
prevLabel="Previous"
|
||||
nextLabel="Next"
|
||||
onPrevClick={() => jest.fn()}
|
||||
onNextClick={() => jest.fn()}
|
||||
onPageClick={(_) => jest.fn()}
|
||||
numRecords={200}
|
||||
numRecordsPerPage={12}
|
||||
activePage={1}
|
||||
paginationRef={createPaginationRef({ page: 1, limit: 12 })}
|
||||
/>,
|
||||
)
|
||||
|
||||
@@ -49,12 +39,8 @@ describe("PaginatedList", () => {
|
||||
<PaginationWidget
|
||||
prevLabel="Previous"
|
||||
nextLabel="Next"
|
||||
onPrevClick={() => jest.fn()}
|
||||
onNextClick={() => jest.fn()}
|
||||
onPageClick={(_) => jest.fn()}
|
||||
numRecords={200}
|
||||
numRecordsPerPage={12}
|
||||
activePage={6}
|
||||
paginationRef={createPaginationRef({ page: 6, limit: 12 })}
|
||||
/>,
|
||||
)
|
||||
|
||||
@@ -63,4 +49,26 @@ describe("PaginatedList", () => {
|
||||
container.querySelectorAll(`button[name="Page button"]`),
|
||||
).toHaveLength(5)
|
||||
})
|
||||
|
||||
it("disables the previous button on the first page", () => {
|
||||
render(
|
||||
<PaginationWidget
|
||||
numRecords={100}
|
||||
paginationRef={createPaginationRef({ page: 1, limit: 25 })}
|
||||
/>,
|
||||
)
|
||||
const prevButton = screen.getByLabelText("Previous page")
|
||||
expect(prevButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it("disables the next button on the last page", () => {
|
||||
render(
|
||||
<PaginationWidget
|
||||
numRecords={100}
|
||||
paginationRef={createPaginationRef({ page: 4, limit: 25 })}
|
||||
/>,
|
||||
)
|
||||
const nextButton = screen.getByLabelText("Next page")
|
||||
expect(nextButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,129 +3,40 @@ import { makeStyles, useTheme } from "@material-ui/core/styles"
|
||||
import useMediaQuery from "@material-ui/core/useMediaQuery"
|
||||
import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft"
|
||||
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
|
||||
import { useActor } from "@xstate/react"
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||
import { Maybe } from "components/Conditionals/Maybe"
|
||||
import { CSSProperties } from "react"
|
||||
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
|
||||
import { PageButton } from "./PageButton"
|
||||
import { buildPagedList } from "./utils"
|
||||
|
||||
export type PaginationWidgetProps = {
|
||||
prevLabel: string
|
||||
nextLabel: string
|
||||
onPrevClick: () => void
|
||||
onNextClick: () => void
|
||||
onPageClick?: (page: number) => void
|
||||
numRecordsPerPage?: number
|
||||
prevLabel?: string
|
||||
nextLabel?: string
|
||||
numRecords?: number
|
||||
activePage?: number
|
||||
containerStyle?: CSSProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a ranged array with an option to step over values.
|
||||
* Shamelessly stolen from:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#sequence_generator_range
|
||||
*/
|
||||
const range = (start: number, stop: number, step = 1) =>
|
||||
Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step)
|
||||
|
||||
export const DEFAULT_RECORDS_PER_PAGE = 25
|
||||
// Number of pages to the left or right of the current page selection.
|
||||
const PAGE_NEIGHBORS = 1
|
||||
// Number of pages displayed for cases where there are multiple ellipsis showing. This can be
|
||||
// thought of as the minimum number of page numbers to display when multiple ellipsis are showing.
|
||||
const PAGES_TO_DISPLAY = PAGE_NEIGHBORS * 2 + 3
|
||||
// Total page blocks(page numbers or ellipsis) displayed, including the maximum number of ellipsis (2).
|
||||
// This gives us maximum number of 7 page blocks to be displayed when the page neighbors value is 1.
|
||||
const NUM_PAGE_BLOCKS = PAGES_TO_DISPLAY + 2
|
||||
|
||||
/**
|
||||
* Builds a list of pages based on how many pages exist and where the user is in their navigation of those pages.
|
||||
* List result is used to from the buttons that make up the Pagination Widget
|
||||
*/
|
||||
export const buildPagedList = (
|
||||
numPages: number,
|
||||
activePage: number,
|
||||
): (string | number)[] => {
|
||||
if (numPages > NUM_PAGE_BLOCKS) {
|
||||
let pages = []
|
||||
const leftBound = activePage - PAGE_NEIGHBORS
|
||||
const rightBound = activePage + PAGE_NEIGHBORS
|
||||
const beforeLastPage = numPages - 1
|
||||
const startPage = leftBound > 2 ? leftBound : 2
|
||||
const endPage = rightBound < beforeLastPage ? rightBound : beforeLastPage
|
||||
|
||||
pages = range(startPage, endPage)
|
||||
|
||||
const singleSpillOffset = PAGES_TO_DISPLAY - pages.length - 1
|
||||
const hasLeftOverflow = startPage > 2
|
||||
const hasRightOverflow = endPage < beforeLastPage
|
||||
const leftOverflowPage = "left"
|
||||
const rightOverflowPage = "right"
|
||||
|
||||
if (hasLeftOverflow && !hasRightOverflow) {
|
||||
const extraPages = range(startPage - singleSpillOffset, startPage - 1)
|
||||
pages = [leftOverflowPage, ...extraPages, ...pages]
|
||||
} else if (!hasLeftOverflow && hasRightOverflow) {
|
||||
const extraPages = range(endPage + 1, endPage + singleSpillOffset)
|
||||
pages = [...pages, ...extraPages, rightOverflowPage]
|
||||
} else if (hasLeftOverflow && hasRightOverflow) {
|
||||
pages = [leftOverflowPage, ...pages, rightOverflowPage]
|
||||
}
|
||||
|
||||
return [1, ...pages, numPages]
|
||||
}
|
||||
|
||||
return range(1, numPages)
|
||||
}
|
||||
|
||||
interface PageButtonProps {
|
||||
activePage: number
|
||||
page: number
|
||||
numPages: number
|
||||
onPageClick?: (page: number) => void
|
||||
}
|
||||
|
||||
const PageButton = ({
|
||||
activePage,
|
||||
page,
|
||||
numPages,
|
||||
onPageClick,
|
||||
}: PageButtonProps): JSX.Element => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<Button
|
||||
className={
|
||||
activePage === page
|
||||
? `${styles.pageButton} ${styles.activePageButton}`
|
||||
: styles.pageButton
|
||||
}
|
||||
aria-label={`${page === activePage ? "Current Page" : ""} ${
|
||||
page === numPages ? "Last Page" : ""
|
||||
} Page${page}`}
|
||||
name="Page button"
|
||||
onClick={() => onPageClick && onPageClick(page)}
|
||||
>
|
||||
<div>{page}</div>
|
||||
</Button>
|
||||
)
|
||||
paginationRef: PaginationMachineRef
|
||||
}
|
||||
|
||||
export const PaginationWidget = ({
|
||||
prevLabel,
|
||||
nextLabel,
|
||||
onPrevClick,
|
||||
onNextClick,
|
||||
onPageClick,
|
||||
prevLabel = "",
|
||||
nextLabel = "",
|
||||
numRecords,
|
||||
numRecordsPerPage = DEFAULT_RECORDS_PER_PAGE,
|
||||
activePage = 1,
|
||||
containerStyle,
|
||||
paginationRef,
|
||||
}: PaginationWidgetProps): JSX.Element | null => {
|
||||
const numPages = numRecords ? Math.ceil(numRecords / numRecordsPerPage) : 0
|
||||
const firstPageActive = activePage === 1 && numPages !== 0
|
||||
const lastPageActive = activePage === numPages && numPages !== 0
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"))
|
||||
const styles = useStyles()
|
||||
const [paginationState, send] = useActor(paginationRef)
|
||||
|
||||
const currentPage = paginationState.context.page
|
||||
const numRecordsPerPage = paginationState.context.limit
|
||||
|
||||
const numPages = numRecords ? Math.ceil(numRecords / numRecordsPerPage) : 0
|
||||
const firstPageActive = currentPage === 1 && numPages !== 0
|
||||
const lastPageActive = currentPage === numPages && numPages !== 0
|
||||
|
||||
// No need to display any pagination if we know the number of pages is 1 or 0
|
||||
if (numPages <= 1 || numRecords === 0) {
|
||||
@@ -138,7 +49,7 @@ export const PaginationWidget = ({
|
||||
className={styles.prevLabelStyles}
|
||||
aria-label="Previous page"
|
||||
disabled={firstPageActive}
|
||||
onClick={onPrevClick}
|
||||
onClick={() => send({ type: "PREVIOUS_PAGE" })}
|
||||
>
|
||||
<KeyboardArrowLeft />
|
||||
<div>{prevLabel}</div>
|
||||
@@ -147,28 +58,27 @@ export const PaginationWidget = ({
|
||||
<ChooseOne>
|
||||
<Cond condition={isMobile}>
|
||||
<PageButton
|
||||
activePage={activePage}
|
||||
page={activePage}
|
||||
activePage={currentPage}
|
||||
page={currentPage}
|
||||
numPages={numPages}
|
||||
/>
|
||||
</Cond>
|
||||
<Cond>
|
||||
{buildPagedList(numPages, activePage).map((page) =>
|
||||
{buildPagedList(numPages, currentPage).map((page) =>
|
||||
typeof page !== "number" ? (
|
||||
<Button
|
||||
className={styles.pageButton}
|
||||
<PageButton
|
||||
key={`Page${page}`}
|
||||
activePage={currentPage}
|
||||
placeholder="..."
|
||||
disabled
|
||||
>
|
||||
<div>...</div>
|
||||
</Button>
|
||||
/>
|
||||
) : (
|
||||
<PageButton
|
||||
key={`Page${page}`}
|
||||
activePage={activePage}
|
||||
activePage={currentPage}
|
||||
page={page}
|
||||
numPages={numPages}
|
||||
onPageClick={onPageClick}
|
||||
onPageClick={() => send({ type: "GO_TO_PAGE", page })}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
@@ -178,7 +88,7 @@ export const PaginationWidget = ({
|
||||
<Button
|
||||
aria-label="Next page"
|
||||
disabled={lastPageActive}
|
||||
onClick={onNextClick}
|
||||
onClick={() => send({ type: "NEXT_PAGE" })}
|
||||
>
|
||||
<div>{nextLabel}</div>
|
||||
<KeyboardArrowRight />
|
||||
@@ -199,15 +109,4 @@ const useStyles = makeStyles((theme) => ({
|
||||
prevLabelStyles: {
|
||||
marginRight: `${theme.spacing(0.5)}px`,
|
||||
},
|
||||
|
||||
pageButton: {
|
||||
"&:not(:last-of-type)": {
|
||||
marginRight: theme.spacing(0.5),
|
||||
},
|
||||
},
|
||||
|
||||
activePageButton: {
|
||||
borderColor: `${theme.palette.info.main}`,
|
||||
backgroundColor: `${theme.palette.info.dark}`,
|
||||
},
|
||||
}))
|
||||
|
||||
+14
-1
@@ -1,4 +1,4 @@
|
||||
import { buildPagedList } from "./PaginationWidget"
|
||||
import { buildPagedList, getOffset } from "./utils"
|
||||
|
||||
describe("unit/PaginationWidget", () => {
|
||||
describe("buildPagedList", () => {
|
||||
@@ -27,3 +27,16 @@ describe("unit/PaginationWidget", () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getOffset", () => {
|
||||
it("returns 0 on page 1", () => {
|
||||
const page = 1
|
||||
const limit = 10
|
||||
expect(getOffset(page, limit)).toEqual(0)
|
||||
})
|
||||
it("returns the limit on page 2", () => {
|
||||
const page = 2
|
||||
const limit = 10
|
||||
expect(getOffset(page, limit)).toEqual(limit)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
PaginationContext,
|
||||
paginationMachine,
|
||||
PaginationMachineRef,
|
||||
} from "xServices/pagination/paginationXService"
|
||||
import { spawn } from "xstate"
|
||||
|
||||
/**
|
||||
* Generates a ranged array with an option to step over values.
|
||||
* Shamelessly stolen from:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#sequence_generator_range
|
||||
*/
|
||||
const range = (start: number, stop: number, step = 1) =>
|
||||
Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step)
|
||||
|
||||
export const DEFAULT_RECORDS_PER_PAGE = 25
|
||||
// Number of pages to the left or right of the current page selection.
|
||||
const PAGE_NEIGHBORS = 1
|
||||
// Number of pages displayed for cases where there are multiple ellipsis showing. This can be
|
||||
// thought of as the minimum number of page numbers to display when multiple ellipsis are showing.
|
||||
const PAGES_TO_DISPLAY = PAGE_NEIGHBORS * 2 + 3
|
||||
// Total page blocks(page numbers or ellipsis) displayed, including the maximum number of ellipsis (2).
|
||||
// This gives us maximum number of 7 page blocks to be displayed when the page neighbors value is 1.
|
||||
const NUM_PAGE_BLOCKS = PAGES_TO_DISPLAY + 2
|
||||
|
||||
/**
|
||||
* Builds a list of pages based on how many pages exist and where the user is in their navigation of those pages.
|
||||
* List result is used to from the buttons that make up the Pagination Widget
|
||||
*/
|
||||
export const buildPagedList = (
|
||||
numPages: number,
|
||||
activePage: number,
|
||||
): (string | number)[] => {
|
||||
if (numPages > NUM_PAGE_BLOCKS) {
|
||||
let pages = []
|
||||
const leftBound = activePage - PAGE_NEIGHBORS
|
||||
const rightBound = activePage + PAGE_NEIGHBORS
|
||||
const beforeLastPage = numPages - 1
|
||||
const startPage = leftBound > 2 ? leftBound : 2
|
||||
const endPage = rightBound < beforeLastPage ? rightBound : beforeLastPage
|
||||
|
||||
pages = range(startPage, endPage)
|
||||
|
||||
const singleSpillOffset = PAGES_TO_DISPLAY - pages.length - 1
|
||||
const hasLeftOverflow = startPage > 2
|
||||
const hasRightOverflow = endPage < beforeLastPage
|
||||
const leftOverflowPage = "left"
|
||||
const rightOverflowPage = "right"
|
||||
|
||||
if (hasLeftOverflow && !hasRightOverflow) {
|
||||
const extraPages = range(startPage - singleSpillOffset, startPage - 1)
|
||||
pages = [leftOverflowPage, ...extraPages, ...pages]
|
||||
} else if (!hasLeftOverflow && hasRightOverflow) {
|
||||
const extraPages = range(endPage + 1, endPage + singleSpillOffset)
|
||||
pages = [...pages, ...extraPages, rightOverflowPage]
|
||||
} else if (hasLeftOverflow && hasRightOverflow) {
|
||||
pages = [leftOverflowPage, ...pages, rightOverflowPage]
|
||||
}
|
||||
|
||||
return [1, ...pages, numPages]
|
||||
}
|
||||
|
||||
return range(1, numPages)
|
||||
}
|
||||
|
||||
const getInitialPage = (page: string | null): number =>
|
||||
page ? Number(page) : 1
|
||||
|
||||
// pages count from 1
|
||||
export const getOffset = (page: number, limit: number): number =>
|
||||
(page - 1) * limit
|
||||
|
||||
interface PaginationData {
|
||||
offset: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
export const getPaginationData = (
|
||||
ref: PaginationMachineRef,
|
||||
): PaginationData => {
|
||||
const snapshot = ref.getSnapshot()
|
||||
if (snapshot) {
|
||||
const { page, limit } = snapshot.context
|
||||
const offset = getOffset(page, limit)
|
||||
return { offset, limit }
|
||||
} else {
|
||||
throw new Error("No pagination data")
|
||||
}
|
||||
}
|
||||
|
||||
export const getPaginationContext = (
|
||||
searchParams: URLSearchParams,
|
||||
limit: number = DEFAULT_RECORDS_PER_PAGE,
|
||||
): PaginationContext => ({
|
||||
page: getInitialPage(searchParams.get("page")),
|
||||
limit,
|
||||
})
|
||||
|
||||
// for storybook
|
||||
export const createPaginationRef = (
|
||||
context: PaginationContext,
|
||||
): PaginationMachineRef => {
|
||||
return spawn(paginationMachine.withContext(context))
|
||||
}
|
||||
@@ -65,5 +65,29 @@ describe("AuditPage", () => {
|
||||
|
||||
expect(getAuditLogsSpy).toBeCalledWith({ limit: 25, offset: 0, q: query })
|
||||
})
|
||||
|
||||
it("resets page to 1 when filter is changed", async () => {
|
||||
const getAuditLogsSpy = jest
|
||||
.spyOn(API, "getAuditLogs")
|
||||
.mockResolvedValue({ audit_logs: [MockAuditLog] })
|
||||
|
||||
history.push(`/audit?page=2`)
|
||||
render(<AuditPage />)
|
||||
|
||||
await waitForLoaderToBeRemoved()
|
||||
getAuditLogsSpy.mockReset()
|
||||
|
||||
const filterField = screen.getByLabelText("Filter")
|
||||
const query = "resource_type:workspace action:create"
|
||||
await userEvent.type(filterField, query)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getAuditLogsSpy).toBeCalledWith({
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
q: query,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { getPaginationContext } from "components/PaginationWidget/utils"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useNavigate, useSearchParams } from "react-router-dom"
|
||||
import { useFilter } from "util/filters"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
import { pageTitle } from "util/page"
|
||||
import { auditMachine } from "xServices/audit/auditXService"
|
||||
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
|
||||
import { AuditPageView } from "./AuditPageView"
|
||||
|
||||
const AuditPage: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const currentPage = searchParams.get("page")
|
||||
? Number(searchParams.get("page"))
|
||||
: 1
|
||||
const { filter, setFilter } = useFilter("")
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const filter = searchParams.get("filter") ?? ""
|
||||
const [auditState, auditSend] = useMachine(auditMachine, {
|
||||
context: {
|
||||
page: currentPage,
|
||||
limit: 25,
|
||||
filter,
|
||||
paginationContext: getPaginationContext(searchParams),
|
||||
},
|
||||
actions: {
|
||||
onPageChange: ({ page }) => {
|
||||
navigate({
|
||||
search: `?page=${page}`,
|
||||
})
|
||||
},
|
||||
updateURL: (context, event) =>
|
||||
setSearchParams({ page: event.page, filter: context.filter }),
|
||||
},
|
||||
})
|
||||
const { auditLogs, count, page, limit } = auditState.context
|
||||
|
||||
const { auditLogs, count } = auditState.context
|
||||
const paginationRef = auditState.context.paginationRef as PaginationMachineRef
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -39,21 +34,10 @@ const AuditPage: FC = () => {
|
||||
filter={filter}
|
||||
auditLogs={auditLogs}
|
||||
count={count}
|
||||
page={page}
|
||||
limit={limit}
|
||||
onNext={() => {
|
||||
auditSend("NEXT")
|
||||
}}
|
||||
onPrevious={() => {
|
||||
auditSend("PREVIOUS")
|
||||
}}
|
||||
onGoToPage={(page) => {
|
||||
auditSend("GO_TO_PAGE", { page })
|
||||
}}
|
||||
onFilter={(filter) => {
|
||||
setFilter(filter)
|
||||
auditSend("FILTER", { filter })
|
||||
}}
|
||||
paginationRef={paginationRef}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { createPaginationRef } from "components/PaginationWidget/utils"
|
||||
import { MockAuditLog, MockAuditLog2 } from "testHelpers/entities"
|
||||
import { AuditPageView, AuditPageViewProps } from "./AuditPageView"
|
||||
|
||||
export default {
|
||||
title: "pages/AuditPageView",
|
||||
component: AuditPageView,
|
||||
argTypes: {
|
||||
auditLogs: {
|
||||
defaultValue: [MockAuditLog, MockAuditLog2],
|
||||
},
|
||||
count: {
|
||||
defaultValue: 1000,
|
||||
},
|
||||
paginationRef: {
|
||||
defaultValue: createPaginationRef({ page: 1, limit: 25 }),
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof AuditPageView>
|
||||
|
||||
const Template: Story<AuditPageViewProps> = (args) => (
|
||||
@@ -12,20 +24,8 @@ const Template: Story<AuditPageViewProps> = (args) => (
|
||||
)
|
||||
|
||||
export const AuditPage = Template.bind({})
|
||||
AuditPage.args = {
|
||||
auditLogs: [MockAuditLog, MockAuditLog2],
|
||||
count: 1000,
|
||||
page: 1,
|
||||
limit: 25,
|
||||
}
|
||||
|
||||
export const AuditPageSmallViewport = Template.bind({})
|
||||
AuditPageSmallViewport.args = {
|
||||
auditLogs: [MockAuditLog, MockAuditLog2],
|
||||
count: 1000,
|
||||
page: 1,
|
||||
limit: 25,
|
||||
}
|
||||
AuditPageSmallViewport.parameters = {
|
||||
chromatic: { viewports: [600] },
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Stack } from "components/Stack/Stack"
|
||||
import { TableLoader } from "components/TableLoader/TableLoader"
|
||||
import { AuditHelpTooltip } from "components/Tooltips"
|
||||
import { FC } from "react"
|
||||
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
|
||||
|
||||
export const Language = {
|
||||
title: "Audit",
|
||||
@@ -39,25 +40,17 @@ const presetFilters = [
|
||||
export interface AuditPageViewProps {
|
||||
auditLogs?: AuditLog[]
|
||||
count?: number
|
||||
page: number
|
||||
limit: number
|
||||
filter: string
|
||||
onFilter: (filter: string) => void
|
||||
onNext: () => void
|
||||
onPrevious: () => void
|
||||
onGoToPage: (page: number) => void
|
||||
paginationRef: PaginationMachineRef
|
||||
}
|
||||
|
||||
export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||
auditLogs,
|
||||
count,
|
||||
page,
|
||||
limit,
|
||||
filter,
|
||||
onFilter,
|
||||
onNext,
|
||||
onPrevious,
|
||||
onGoToPage,
|
||||
paginationRef,
|
||||
}) => {
|
||||
const isLoading = auditLogs === undefined || count === undefined
|
||||
const isEmpty = !isLoading && auditLogs.length === 0
|
||||
@@ -106,18 +99,7 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{count && count > limit ? (
|
||||
<PaginationWidget
|
||||
prevLabel=""
|
||||
nextLabel=""
|
||||
onPrevClick={onPrevious}
|
||||
onNextClick={onNext}
|
||||
onPageClick={onGoToPage}
|
||||
numRecords={count}
|
||||
activePage={page}
|
||||
numRecordsPerPage={limit}
|
||||
/>
|
||||
) : null}
|
||||
<PaginationWidget numRecords={count} paginationRef={paginationRef} />
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,43 +1,33 @@
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/PaginationWidget"
|
||||
import { getPaginationContext } from "components/PaginationWidget/utils"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useNavigate, useSearchParams } from "react-router-dom"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
import { workspaceFilterQuery } from "util/filters"
|
||||
import { pageTitle } from "util/page"
|
||||
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
|
||||
import { workspacesMachine } from "xServices/workspaces/workspacesXService"
|
||||
import { WorkspacesPageView } from "./WorkspacesPageView"
|
||||
|
||||
const WorkspacesPage: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const filter = searchParams.get("filter") ?? workspaceFilterQuery.me
|
||||
const currentPage = searchParams.get("page")
|
||||
? Number(searchParams.get("page"))
|
||||
: 1
|
||||
const [workspacesState, send] = useMachine(workspacesMachine, {
|
||||
context: {
|
||||
page: currentPage,
|
||||
limit: DEFAULT_RECORDS_PER_PAGE,
|
||||
filter,
|
||||
paginationContext: getPaginationContext(searchParams),
|
||||
},
|
||||
actions: {
|
||||
onPageChange: ({ page }) => {
|
||||
navigate({
|
||||
search: `?page=${page}`,
|
||||
})
|
||||
},
|
||||
// Filter updates always cause page updates (to page 1), so only UPDATE_PAGE triggers updateURL
|
||||
updateURL: (context, event) =>
|
||||
setSearchParams({ page: event.page, filter: context.filter }),
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
workspaceRefs,
|
||||
count,
|
||||
page,
|
||||
limit,
|
||||
getWorkspacesError,
|
||||
getCountError,
|
||||
} = workspacesState.context
|
||||
const { workspaceRefs, count, getWorkspacesError, getCountError } =
|
||||
workspacesState.context
|
||||
const paginationRef = workspacesState.context
|
||||
.paginationRef as PaginationMachineRef
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -52,24 +42,13 @@ const WorkspacesPage: FC = () => {
|
||||
count={count}
|
||||
getWorkspacesError={getWorkspacesError}
|
||||
getCountError={getCountError}
|
||||
page={page}
|
||||
limit={limit}
|
||||
onNext={() => {
|
||||
send("NEXT")
|
||||
}}
|
||||
onPrevious={() => {
|
||||
send("PREVIOUS")
|
||||
}}
|
||||
onGoToPage={(page) => {
|
||||
send("GO_TO_PAGE", { page })
|
||||
}}
|
||||
onFilter={(query) => {
|
||||
setSearchParams({ filter: query })
|
||||
send({
|
||||
type: "UPDATE_FILTER",
|
||||
query,
|
||||
})
|
||||
}}
|
||||
paginationRef={paginationRef}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { createPaginationRef } from "components/PaginationWidget/utils"
|
||||
import dayjs from "dayjs"
|
||||
import { spawn } from "xstate"
|
||||
import {
|
||||
@@ -87,6 +88,9 @@ export default {
|
||||
title: "pages/WorkspacesPageView",
|
||||
component: WorkspacesPageView,
|
||||
argTypes: {
|
||||
paginationRef: {
|
||||
defaultValue: createPaginationRef({ page: 1, limit: 25 }),
|
||||
},
|
||||
workspaceRefs: {
|
||||
options: [
|
||||
...Object.keys(workspaces),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Maybe } from "components/Conditionals/Maybe"
|
||||
import { PaginationWidget } from "components/PaginationWidget/PaginationWidget"
|
||||
import { FC } from "react"
|
||||
import { Link as RouterLink } from "react-router-dom"
|
||||
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import {
|
||||
PageHeader,
|
||||
@@ -32,13 +33,9 @@ export interface WorkspacesPageViewProps {
|
||||
count?: number
|
||||
getWorkspacesError: Error | unknown
|
||||
getCountError: Error | unknown
|
||||
page: number
|
||||
limit: number
|
||||
filter?: string
|
||||
onFilter: (query: string) => void
|
||||
onNext: () => void
|
||||
onPrevious: () => void
|
||||
onGoToPage: (page: number) => void
|
||||
paginationRef: PaginationMachineRef
|
||||
}
|
||||
|
||||
export const WorkspacesPageView: FC<
|
||||
@@ -49,13 +46,9 @@ export const WorkspacesPageView: FC<
|
||||
count,
|
||||
getWorkspacesError,
|
||||
getCountError,
|
||||
page,
|
||||
limit,
|
||||
filter,
|
||||
onFilter,
|
||||
onNext,
|
||||
onPrevious,
|
||||
onGoToPage,
|
||||
paginationRef,
|
||||
}) => {
|
||||
const presetFilters = [
|
||||
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
|
||||
@@ -114,16 +107,7 @@ export const WorkspacesPageView: FC<
|
||||
filter={filter}
|
||||
/>
|
||||
|
||||
<PaginationWidget
|
||||
prevLabel=""
|
||||
nextLabel=""
|
||||
onPrevClick={onPrevious}
|
||||
onNextClick={onNext}
|
||||
onPageClick={onGoToPage}
|
||||
numRecords={count}
|
||||
activePage={page}
|
||||
numRecordsPerPage={limit}
|
||||
/>
|
||||
<PaginationWidget numRecords={count} paginationRef={paginationRef} />
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,22 @@ import { getAuditLogs, getAuditLogsCount } from "api/api"
|
||||
import { getErrorMessage } from "api/errors"
|
||||
import { AuditLog } from "api/typesGenerated"
|
||||
import { displayError } from "components/GlobalSnackbar/utils"
|
||||
import { assign, createMachine } from "xstate"
|
||||
import { getPaginationData } from "components/PaginationWidget/utils"
|
||||
import {
|
||||
PaginationContext,
|
||||
PaginationMachineRef,
|
||||
paginationMachine,
|
||||
} from "xServices/pagination/paginationXService"
|
||||
import { assign, createMachine, spawn, send } from "xstate"
|
||||
|
||||
const auditPaginationId = "auditPagination"
|
||||
|
||||
interface AuditContext {
|
||||
auditLogs?: AuditLog[]
|
||||
count?: number
|
||||
page: number
|
||||
limit: number
|
||||
filter: string
|
||||
paginationContext: PaginationContext
|
||||
paginationRef?: PaginationMachineRef
|
||||
}
|
||||
|
||||
export const auditMachine = createMachine(
|
||||
@@ -29,22 +37,20 @@ export const auditMachine = createMachine(
|
||||
},
|
||||
events: {} as
|
||||
| {
|
||||
type: "NEXT"
|
||||
}
|
||||
| {
|
||||
type: "PREVIOUS"
|
||||
}
|
||||
| {
|
||||
type: "GO_TO_PAGE"
|
||||
page: number
|
||||
type: "UPDATE_PAGE"
|
||||
page: string
|
||||
}
|
||||
| {
|
||||
type: "FILTER"
|
||||
filter: string
|
||||
},
|
||||
},
|
||||
initial: "loading",
|
||||
initial: "startPagination",
|
||||
states: {
|
||||
startPagination: {
|
||||
entry: "assignPaginationRef",
|
||||
always: "loading",
|
||||
},
|
||||
loading: {
|
||||
// Right now, XState doesn't a good job with state + context typing so
|
||||
// this forces the AuditPageView to showing the loading state when the
|
||||
@@ -65,21 +71,12 @@ export const auditMachine = createMachine(
|
||||
},
|
||||
success: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: ["assignNextPage", "onPageChange"],
|
||||
target: "loading",
|
||||
},
|
||||
PREVIOUS: {
|
||||
actions: ["assignPreviousPage", "onPageChange"],
|
||||
target: "loading",
|
||||
},
|
||||
GO_TO_PAGE: {
|
||||
actions: ["assignPage", "onPageChange"],
|
||||
UPDATE_PAGE: {
|
||||
actions: ["updateURL"],
|
||||
target: "loading",
|
||||
},
|
||||
FILTER: {
|
||||
actions: ["assignFilter"],
|
||||
target: "loading",
|
||||
actions: ["assignFilter", "sendResetPage"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -97,14 +94,12 @@ export const auditMachine = createMachine(
|
||||
auditLogs: (_, event) => event.data.auditLogs,
|
||||
count: (_, event) => event.data.count,
|
||||
}),
|
||||
assignNextPage: assign({
|
||||
page: ({ page }) => page + 1,
|
||||
}),
|
||||
assignPreviousPage: assign({
|
||||
page: ({ page }) => page - 1,
|
||||
}),
|
||||
assignPage: assign({
|
||||
page: (_, { page }) => page,
|
||||
assignPaginationRef: assign({
|
||||
paginationRef: (context) =>
|
||||
spawn(
|
||||
paginationMachine.withContext(context.paginationContext),
|
||||
auditPaginationId,
|
||||
),
|
||||
}),
|
||||
assignFilter: assign({
|
||||
filter: (_, { filter }) => filter,
|
||||
@@ -116,24 +111,29 @@ export const auditMachine = createMachine(
|
||||
)
|
||||
displayError(message)
|
||||
},
|
||||
sendResetPage: send({ type: "RESET_PAGE" }, { to: auditPaginationId }),
|
||||
},
|
||||
services: {
|
||||
loadAuditLogsAndCount: async ({ page, limit, filter }, _) => {
|
||||
const [auditLogs, count] = await Promise.all([
|
||||
getAuditLogs({
|
||||
// The page in the API starts at 0
|
||||
offset: (page - 1) * limit,
|
||||
limit,
|
||||
q: filter,
|
||||
}).then((data) => data.audit_logs),
|
||||
getAuditLogsCount({
|
||||
q: filter,
|
||||
}).then((data) => data.count),
|
||||
])
|
||||
loadAuditLogsAndCount: async (context) => {
|
||||
if (context.paginationRef) {
|
||||
const { offset, limit } = getPaginationData(context.paginationRef)
|
||||
const [auditLogs, count] = await Promise.all([
|
||||
getAuditLogs({
|
||||
offset,
|
||||
limit,
|
||||
q: context.filter,
|
||||
}).then((data) => data.audit_logs),
|
||||
getAuditLogsCount({
|
||||
q: context.filter,
|
||||
}).then((data) => data.count),
|
||||
])
|
||||
|
||||
return {
|
||||
auditLogs,
|
||||
count,
|
||||
return {
|
||||
auditLogs,
|
||||
count,
|
||||
}
|
||||
} else {
|
||||
throw new Error("Cannot get audit logs without pagination data")
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ActorRefFrom, createMachine, sendParent, assign } from "xstate"
|
||||
|
||||
export interface PaginationContext {
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
export type PaginationEvent =
|
||||
| { type: "NEXT_PAGE" }
|
||||
| { type: "PREVIOUS_PAGE" }
|
||||
| { type: "GO_TO_PAGE"; page: number }
|
||||
| { type: "RESET_PAGE" }
|
||||
|
||||
export type PaginationMachineRef = ActorRefFrom<typeof paginationMachine>
|
||||
|
||||
export const paginationMachine =
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QAcCGUCWA7VAXDA9lgLKoDGAFtmAMQByAogBoAqA+gAoCCA4gwNoAGALqIUBWBnxExIAB6IATADZBAOgAcGgCwBWDYMEHBAdm2KANCACeiEwEZ7axboCcbkw8-3XGgL5+VmiYONIk5FRYtBwASgwAagCSAPIAqgDKnLwCIrLIElKEWLIKCAC09hrKarq6AMwarsraGormuiYaVrYIDk4u7q7e3r4BQejYeEWklNQ0PMlsLIvcfEKiSCD5kmElduq69vrKyiaCjqfu3YgauupatYp1eia+o4FbE6HTEXNx6Qx2KschtxDsintyvZqlVzmdGs0tIpbtcENplIoanV7M8jrpFCZntoAh8sAQIHA8l8pkQZpEwGoAE5gVAQHpgwoyTalZSuNTuV6CFzabSCFqdVFmdTPMU+dHnZrKMafEI08KzKJ5Aq7blKJwC1xC3QisUaCU2RDKQ5qGXaOWqaHokl+IA */
|
||||
createMachine(
|
||||
{
|
||||
tsTypes: {} as import("./paginationXService.typegen").Typegen0,
|
||||
schema: {
|
||||
context: {} as PaginationContext,
|
||||
events: {} as PaginationEvent,
|
||||
},
|
||||
predictableActionArguments: true,
|
||||
id: "paginationMachine",
|
||||
initial: "ready",
|
||||
on: {
|
||||
NEXT_PAGE: {
|
||||
actions: ["assignNextPage", "sendUpdatePage"],
|
||||
},
|
||||
PREVIOUS_PAGE: {
|
||||
actions: ["assignPreviousPage", "sendUpdatePage"],
|
||||
},
|
||||
GO_TO_PAGE: {
|
||||
actions: ["assignPage", "sendUpdatePage"],
|
||||
},
|
||||
RESET_PAGE: {
|
||||
actions: ["resetPage", "sendUpdatePage"],
|
||||
},
|
||||
},
|
||||
states: {
|
||||
ready: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
sendUpdatePage: sendParent((context) => ({
|
||||
type: "UPDATE_PAGE",
|
||||
page: context.page.toString(),
|
||||
})),
|
||||
assignNextPage: assign({
|
||||
page: (context) => context.page + 1,
|
||||
}),
|
||||
assignPreviousPage: assign({
|
||||
page: (context) => context.page - 1,
|
||||
}),
|
||||
assignPage: assign({
|
||||
page: (_, event) => event.page,
|
||||
}),
|
||||
resetPage: assign({
|
||||
page: (_) => 1,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -1,4 +1,10 @@
|
||||
import { ActorRefFrom, assign, createMachine, spawn } from "xstate"
|
||||
import { getPaginationData } from "components/PaginationWidget/utils"
|
||||
import {
|
||||
PaginationContext,
|
||||
paginationMachine,
|
||||
PaginationMachineRef,
|
||||
} from "xServices/pagination/paginationXService"
|
||||
import { ActorRefFrom, assign, createMachine, spawn, send } from "xstate"
|
||||
import * as API from "../../api/api"
|
||||
import { getErrorMessage } from "../../api/errors"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
@@ -9,6 +15,8 @@ import {
|
||||
} from "../../components/GlobalSnackbar/utils"
|
||||
import { queryToFilter } from "../../util/filters"
|
||||
|
||||
export const workspacePaginationId = "workspacePagination"
|
||||
|
||||
/**
|
||||
* Workspace item machine
|
||||
*
|
||||
@@ -204,23 +212,21 @@ export type WorkspaceItemMachineRef = ActorRefFrom<typeof workspaceItemMachine>
|
||||
|
||||
interface WorkspacesContext {
|
||||
workspaceRefs?: WorkspaceItemMachineRef[]
|
||||
paginationRef?: PaginationMachineRef
|
||||
filter: string
|
||||
count?: number
|
||||
getWorkspacesError?: Error | unknown
|
||||
getCountError?: Error | unknown
|
||||
page: number
|
||||
count?: number
|
||||
limit: number
|
||||
paginationContext: PaginationContext
|
||||
}
|
||||
|
||||
type WorkspacesEvent =
|
||||
| { type: "UPDATE_FILTER"; query?: string }
|
||||
| { type: "UPDATE_PAGE"; page: string }
|
||||
| { type: "UPDATE_VERSION"; workspaceId: string }
|
||||
| { type: "NEXT" }
|
||||
| { type: "PREVIOUS" }
|
||||
| { type: "GO_TO_PAGE"; page: number }
|
||||
| { type: "UPDATE_FILTER"; query?: string }
|
||||
|
||||
export const workspacesMachine =
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QHcD2AnA1rADgQwGM4BlAFz1LADpk8BLUgFVQCUwAzdOACwHUNs+IrADEAD1jlKVPO0roAFAFYADGoCUItFlyESU6rQbM2nHvx1C4AbRUBdRKBypYDOqgB2jkGMQBGAGYANgCqFQAmAE4-cIAOOPDwoIB2ZIAaEABPRHCVKljAv2S-EMiClSUAFliAXxqM7UE9WDIKanYwUgJuOg8oKgJUAFcPUioYUlJeqABhYdGRCE9qXoA3VExqCYsm4TmR0lsHJBBnVynPb18ESsq8uOLk2KCVSqVY4qUM7IQAyqoApElOFKskgpEUn5avUQI1dMJWtIOl0en0BvMxhMpn19gswOh0BgqDgADYUdgYAC2406O3hcFxh3s3jObkuJ2ufyUVEiAXi4QCySUAWBARF3xyaiowuqZShQSCiUidQaAnpLQMVGR3WmNDVVlgVCGOAgFGmdKsplESw8Kw8602RpNbQteitRxZLjZXg5iFitwByTKARUQVulQCiQlCBeeX9sWifihhXBKth+uaiPanR1aLhBppk3NGeEi2WVDWGy2tJLNmZJ1ZFx9oGub25f0jyQqSiUQvi0aUiqoiXCfnecT8kRUwTT+czmu1qP6c+EhexUFdpZtdod1dIm5sfmOTi9TauiFukWlCb5097-tD0cqQ57dw+8cCz9ntY1bS1OaXPVLGaNdi2A0t8UJdBiTJUgKXQalth-D0G1Pdxmx8fximHF5O1uVJKgFAJoxCZIqChPlIhBUMngib9wP0P9F2mMtbSoSQ-xXRikQA6YUJPc50PPBBAhSYcQWBJJcmBfsshyIpxNHLsCiUIFIyCejdm4sARAAcQAUUYAB9XgAHkWAAaWIAAFABBGZ9OIfjTjQ9kW0QABaQiwmKadkhDQjgmSSpow8gIx3Ivkg17KIwSUPxNPVLMRAAVWsgARWzGH0oyADEAEkABlspYZzGyE30ECePwAVicLkiiJNKjHcJQvC-5Xg+Io+XHYFEoNZK0sy7KjIANX0lhiHy0yADkytcjDrj8NQyNHBUilUYplsiNrvJFJJVEHZqew0mEuN-SgRBm-SAA1GHmwS3MwkSgm5XsFQO4E-FBQI2u+sJqgaiFAeKV7+vnNoRGslh9NG6aUqc+sBO9YTBX+cFR1HQdIgjI6-o6wGojDD5QaUcGEQMPTTKMxhqbsgyHpRyr3iCKhgtE0E7gFZ58YBj4iZBkoybTDxUAgOBvHOrMaHoJhWA4LhYD4H9PUexb-DFdHe26p44hFPxo1EnkgihfzBV5QI+rOn9peYtFBgOUCcQxVWmfc35-nCVTYh9iFwpBEFo0BAEp3iCJRzUEVKnJ7T-xRXUHdGKht1ds9KpCVnXj+V5ihBD7ozWtnEnlPluZ7AIY4u7N4-tl3ULV4SPMCNm7iTZbgg+d7QqqKgXlBYIIxUSIcd5Svbd4vMfydU11wPK1U4q926vCHkdeFKiXjHJ9qjZwcffUwEVGW4XVQYqu49zZcp6xMCtPgeu3eev5pVSSdEjFRUShKbfuSFIJ339hCL2p1T533HjXK+Z9k7LAXk9a4xRPZ3F8tzMUEJoz+TeikXkipQx7wrtbM+4DL5ATvrA9WCAm6hEInEFQqQFSCjqv6IOg5d7-zeIEP4pQx4LgnlAMhjcTYtyPkmac-8hRglCt9UIfcGrdg+LQj43C2j8Mqk3IeQi26iM7hIuSFC7j-ECD7LOdVBRDzqHUIAA */
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QHcD2AnA1rADgQwGM4BlAFz1LADoDUBXAO1KplNIEsGoBhepgYgioG1TgDdUmaqwDqGbPiKxejUgG0ADAF1EoHKljsOw3SAAeiAIyWNVAMwA2AJwAOACyWnAJjsuvGty8AGhAAT0QfWxcAVm9ohwcXAHYkp0sXFwBfTJC0LFxCEnJKGj5mVg4uFQEwdHQMKhwAGwoAMwwAWxYwUjl8xThq9W1TfUNjBlMLBDsnaKovaMtfDQ00jLckhxDwhEiqGO9-OxPZk69s3PkCpTIKalpVfgBVAAUAEQBBABUAUQB9ABiAEkADJ-ABKmh0SBAYyM7BMsOmbm8VA00Q0KRcli2di86TcOwiDioDk8DjcMS8Lm8dg0LgclxAeQUhVgdxKrJucCosHI6EqUFeeCgnAoiIY-GhowMCKRoGmAFocU57J4vG58VSMqliQglR4FmsvF5sX5XMloszuQMOcVqLb2d02JwoH02UpBMJRAwJFIXR6ebAZbD4RMpoglfF5gSGWt8ZYdZZ9ekXFQktEXI58dn3B4LjkWdc7ZzHSXnRU3UG7d6RFRxJJpD0a+y1JYYXo5RHkVHGZYqE43AEYgnnNE7PqaUl0dFTU4NA5Ui41m4bRXbg6qE6lC6ha2vbV6uhGi1SO10F1ZBu4KGu+NJZGDdFAmTacscTSnLn9RkFvS-CTKlNmia0ix3Ip7m3G9YCoOgcAgCUuAPMAITAVpYDrX1-WoeDEMoFC0Iwu84W7R9ewNelSQ0JNyQ0Al4hsE59XOKg3GHId0gcHwkgyMCrn6dky2gwTd2QPAjG+VAiPQOAAAsUMwsx+SgvBWkodAAApMVWABKfgIPtKDDO3CTSCkmT5MUkjw3IxUoznGdVksHw7FA1xZicFi1QcRccVpAleIcF911EyCuRgl4Ph+AFXk+ABxX4bLIhVzAc78DjsawHDc7wdW2MIo2WNVeNNTMaTNLLF1Cz1wrAKKvj+f4ADVfghYhgQAeQAOWSh9UumWYM2cWjVixdxJ0Kg1NXmXzEhcjF3Bo7IiwYVAIDgUxDOEx4mAbCAmjAWV+smCj-HTLK50xZxFg0WZgimxwSrcSkkheuY3GiJJCwE2qjJKXbyh6IUhmO+VTvshBuIORJKQJHx4esKcfOOfwEi-OxUhq4MdrKMGe0hpMYcZQIXMTAkXP1JVoZWNyGSTQkMX44swv+8tWb5AUhRFMUGAlVLbIGvtGSoSxYj8LNPretIqaTWwvG-JwcvYrxfJfH6Wb+4STKrZCYPxuy0oNWlYxiNyTi1fx0inUlYnJJwHY8dwJ3ibHSy3Ey8KQ90byI+AwxSiGjaVJJrCoJYGUCF9s3xFjUTJD7VdcdItjFt2hI9mDTMk6T0Nk2AFP1gOTqfJU3IHb7UmxSI3qxFi7AWKuFbSBImacdPN2Mov73B0uhwbmkXYt-EaJcKn5wWRXuN4r6XNAju6oNoWDQdgdB-NuxLdHqmsvTBXZm4lyMgpJkVqAA */
|
||||
createMachine(
|
||||
{
|
||||
tsTypes: {} as import("./workspacesXService.typegen").Typegen1,
|
||||
@@ -245,113 +251,102 @@ export const workspacesMachine =
|
||||
predictableActionArguments: true,
|
||||
id: "workspacesState",
|
||||
on: {
|
||||
UPDATE_FILTER: {
|
||||
target: ".fetching",
|
||||
actions: ["assignFilter", "resetPage"],
|
||||
},
|
||||
UPDATE_VERSION: {
|
||||
actions: "triggerUpdateVersion",
|
||||
},
|
||||
NEXT: {
|
||||
target: ".fetching",
|
||||
actions: ["assignNextPage", "onPageChange"],
|
||||
},
|
||||
PREVIOUS: {
|
||||
target: ".fetching",
|
||||
actions: ["assignPreviousPage", "onPageChange"],
|
||||
},
|
||||
GO_TO_PAGE: {
|
||||
target: ".fetching",
|
||||
actions: ["assignPage", "onPageChange"],
|
||||
},
|
||||
},
|
||||
initial: "fetching",
|
||||
type: "parallel",
|
||||
states: {
|
||||
waitToRefreshWorkspaces: {
|
||||
after: {
|
||||
"5000": {
|
||||
target: "#workspacesState.fetching",
|
||||
actions: [],
|
||||
internal: false,
|
||||
count: {
|
||||
initial: "gettingCount",
|
||||
states: {
|
||||
idle: {},
|
||||
gettingCount: {
|
||||
entry: "clearGetCountError",
|
||||
invoke: {
|
||||
src: "getWorkspacesCount",
|
||||
id: "getWorkspacesCount",
|
||||
onDone: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: "assignCount",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: "assignGetCountError",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
on: {
|
||||
UPDATE_FILTER: {
|
||||
target: ".gettingCount",
|
||||
actions: ["assignFilter", "sendResetPage"],
|
||||
},
|
||||
},
|
||||
},
|
||||
fetching: {
|
||||
type: "parallel",
|
||||
workspaces: {
|
||||
initial: "startingPagination",
|
||||
states: {
|
||||
count: {
|
||||
initial: "gettingCount",
|
||||
states: {
|
||||
gettingCount: {
|
||||
entry: "clearGetCountError",
|
||||
invoke: {
|
||||
src: "getWorkspacesCount",
|
||||
id: "getWorkspacesCount",
|
||||
onDone: [
|
||||
{
|
||||
target: "done",
|
||||
actions: "assignCount",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "done",
|
||||
actions: "assignGetCountError",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
done: {
|
||||
type: "final",
|
||||
},
|
||||
startingPagination: {
|
||||
entry: "assignPaginationRef",
|
||||
always: {
|
||||
target: "gettingWorkspaces",
|
||||
},
|
||||
},
|
||||
workspaces: {
|
||||
initial: "gettingWorkspaces",
|
||||
states: {
|
||||
updatingWorkspaceRefs: {
|
||||
invoke: {
|
||||
src: "updateWorkspaceRefs",
|
||||
id: "updateWorkspaceRefs",
|
||||
onDone: [
|
||||
{
|
||||
target: "done",
|
||||
actions: "assignUpdatedWorkspaceRefs",
|
||||
},
|
||||
],
|
||||
gettingWorkspaces: {
|
||||
entry: "clearGetWorkspacesError",
|
||||
invoke: {
|
||||
src: "getWorkspaces",
|
||||
id: "getWorkspaces",
|
||||
onDone: [
|
||||
{
|
||||
target: "waitToRefreshWorkspaces",
|
||||
cond: "isEmpty",
|
||||
actions: "assignWorkspaceRefs",
|
||||
},
|
||||
},
|
||||
gettingWorkspaces: {
|
||||
entry: "clearGetWorkspacesError",
|
||||
invoke: {
|
||||
src: "getWorkspaces",
|
||||
id: "getWorkspaces",
|
||||
onDone: [
|
||||
{
|
||||
target: "done",
|
||||
cond: "isEmpty",
|
||||
actions: "assignWorkspaceRefs",
|
||||
},
|
||||
{
|
||||
target: "updatingWorkspaceRefs",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "done",
|
||||
actions: "assignGetWorkspacesError",
|
||||
},
|
||||
],
|
||||
{
|
||||
target: "updatingWorkspaceRefs",
|
||||
},
|
||||
},
|
||||
done: {
|
||||
type: "final",
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "waitToRefreshWorkspaces",
|
||||
actions: "assignGetWorkspacesError",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
updatingWorkspaceRefs: {
|
||||
invoke: {
|
||||
src: "updateWorkspaceRefs",
|
||||
id: "updateWorkspaceRefs",
|
||||
onDone: [
|
||||
{
|
||||
target: "waitToRefreshWorkspaces",
|
||||
actions: "assignUpdatedWorkspaceRefs",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
waitToRefreshWorkspaces: {
|
||||
after: {
|
||||
"5000": {
|
||||
target: "#workspacesState.workspaces.gettingWorkspaces",
|
||||
actions: [],
|
||||
internal: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
onDone: {
|
||||
target: "waitToRefreshWorkspaces",
|
||||
on: {
|
||||
UPDATE_PAGE: {
|
||||
target: ".gettingWorkspaces",
|
||||
actions: "updateURL",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -367,9 +362,20 @@ export const workspacesMachine =
|
||||
return spawn(workspaceItemMachine.withContext({ data }), data.id)
|
||||
}),
|
||||
}),
|
||||
assignPaginationRef: assign({
|
||||
paginationRef: (context) =>
|
||||
spawn(
|
||||
paginationMachine.withContext(context.paginationContext),
|
||||
workspacePaginationId,
|
||||
),
|
||||
}),
|
||||
assignFilter: assign({
|
||||
filter: (context, event) => event.query ?? context.filter,
|
||||
}),
|
||||
sendResetPage: send(
|
||||
{ type: "RESET_PAGE" },
|
||||
{ to: workspacePaginationId },
|
||||
),
|
||||
assignGetWorkspacesError: assign({
|
||||
getWorkspacesError: (_, event) => event.data,
|
||||
}),
|
||||
@@ -397,18 +403,6 @@ export const workspacesMachine =
|
||||
return event.data.refsToKeep.concat(newWorkspaceRefs)
|
||||
},
|
||||
}),
|
||||
assignNextPage: assign({
|
||||
page: (context) => context.page + 1,
|
||||
}),
|
||||
assignPreviousPage: assign({
|
||||
page: (context) => context.page - 1,
|
||||
}),
|
||||
assignPage: assign({
|
||||
page: (_, event) => event.page,
|
||||
}),
|
||||
resetPage: assign({
|
||||
page: (_) => 1,
|
||||
}),
|
||||
assignCount: assign({
|
||||
count: (_, event) => event.data.count,
|
||||
}),
|
||||
@@ -420,12 +414,18 @@ export const workspacesMachine =
|
||||
}),
|
||||
},
|
||||
services: {
|
||||
getWorkspaces: (context) =>
|
||||
API.getWorkspaces({
|
||||
...queryToFilter(context.filter),
|
||||
offset: (context.page - 1) * context.limit,
|
||||
limit: context.limit,
|
||||
}),
|
||||
getWorkspaces: (context) => {
|
||||
if (context.paginationRef) {
|
||||
const { offset, limit } = getPaginationData(context.paginationRef)
|
||||
return API.getWorkspaces({
|
||||
...queryToFilter(context.filter),
|
||||
offset,
|
||||
limit,
|
||||
})
|
||||
} else {
|
||||
throw new Error("Cannot get workspaces without pagination data")
|
||||
}
|
||||
},
|
||||
updateWorkspaceRefs: (context, event) => {
|
||||
const refsToKeep: WorkspaceItemMachineRef[] = []
|
||||
context.workspaceRefs?.forEach((ref) => {
|
||||
|
||||
Reference in New Issue
Block a user