mirror of https://github.com/coder/coder.git
chore(site): Use react-query and refactor the workspaces page to use it (#5838)
This commit is contained in:
parent
bef9e72078
commit
36384aa3c1
|
@ -37,7 +37,6 @@ rules:
|
|||
["error", "1tbs", { "allowSingleLine": false }]
|
||||
"@typescript-eslint/camelcase": "off"
|
||||
"@typescript-eslint/explicit-function-return-type": "off"
|
||||
"@typescript-eslint/explicit-module-boundary-types": "error"
|
||||
"@typescript-eslint/method-signature-style": ["error", "property"]
|
||||
"@typescript-eslint/no-floating-promises": error
|
||||
"@typescript-eslint/no-invalid-void-type": error
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
"@material-ui/icons": "4.5.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.42",
|
||||
"@monaco-editor/react": "4.4.6",
|
||||
"@tanstack/react-query": "4.22.4",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/color-convert": "2.0.0",
|
||||
"@types/react-color": "3.0.6",
|
||||
|
|
|
@ -779,3 +779,10 @@ export const getTemplateVersionLogs = async (
|
|||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const updateWorkspaceVersion = async (
|
||||
workspace: TypesGen.Workspace,
|
||||
): Promise<TypesGen.WorkspaceBuild> => {
|
||||
const template = await getTemplate(workspace.template_id)
|
||||
return startWorkspace(workspace.id, template.active_version_id)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import CssBaseline from "@material-ui/core/CssBaseline"
|
||||
import ThemeProvider from "@material-ui/styles/ThemeProvider"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { AuthProvider } from "components/AuthProvider/AuthProvider"
|
||||
import { FC } from "react"
|
||||
import { FC, PropsWithChildren } from "react"
|
||||
import { HelmetProvider } from "react-helmet-async"
|
||||
import { AppRouter } from "./AppRouter"
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary"
|
||||
|
@ -9,18 +10,37 @@ import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar"
|
|||
import { dark } from "./theme"
|
||||
import "./theme/globalFonts"
|
||||
|
||||
export const App: FC = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
cacheTime: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const AppProviders: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<ThemeProvider theme={dark}>
|
||||
<CssBaseline />
|
||||
<ErrorBoundary>
|
||||
<AuthProvider>
|
||||
<AppRouter />
|
||||
<GlobalSnackbar />
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
<GlobalSnackbar />
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
</ThemeProvider>
|
||||
</HelmetProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const App: FC = () => {
|
||||
return (
|
||||
<AppProviders>
|
||||
<AppRouter />
|
||||
</AppProviders>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import Button from "@material-ui/core/Button"
|
||||
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 { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||
import { PageButton } from "./PageButton"
|
||||
import { buildPagedList } from "./utils"
|
||||
|
||||
export type PaginationWidgetBaseProps = {
|
||||
count: number
|
||||
page: number
|
||||
limit: number
|
||||
onChange: (page: number) => void
|
||||
}
|
||||
|
||||
export const PaginationWidgetBase = ({
|
||||
count,
|
||||
page,
|
||||
limit,
|
||||
onChange,
|
||||
}: PaginationWidgetBaseProps): JSX.Element | null => {
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"))
|
||||
const styles = useStyles()
|
||||
const numPages = Math.ceil(count / limit)
|
||||
const isFirstPage = page === 0
|
||||
const isLastPage = page === numPages - 1
|
||||
|
||||
if (numPages < 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.defaultContainerStyles}>
|
||||
<Button
|
||||
className={styles.prevLabelStyles}
|
||||
aria-label="Previous page"
|
||||
disabled={isFirstPage}
|
||||
onClick={() => {
|
||||
if (!isFirstPage) {
|
||||
onChange(page - 1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<KeyboardArrowLeft />
|
||||
</Button>
|
||||
<ChooseOne>
|
||||
<Cond condition={isMobile}>
|
||||
<PageButton activePage={page} page={page} numPages={numPages} />
|
||||
</Cond>
|
||||
<Cond>
|
||||
{buildPagedList(numPages, page).map((pageItem) => {
|
||||
if (pageItem === "left" || pageItem === "right") {
|
||||
return (
|
||||
<PageButton
|
||||
key={pageItem}
|
||||
activePage={page}
|
||||
placeholder="..."
|
||||
disabled
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageButton
|
||||
key={pageItem}
|
||||
page={pageItem}
|
||||
activePage={page}
|
||||
numPages={numPages}
|
||||
onPageClick={() => onChange(pageItem)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
<Button
|
||||
aria-label="Next page"
|
||||
disabled={isLastPage}
|
||||
onClick={() => {
|
||||
if (!isLastPage) {
|
||||
onChange(page + 1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<KeyboardArrowRight />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
defaultContainerStyles: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
padding: "20px",
|
||||
},
|
||||
|
||||
prevLabelStyles: {
|
||||
marginRight: `${theme.spacing(0.5)}px`,
|
||||
},
|
||||
}))
|
|
@ -30,7 +30,7 @@ const NUM_PAGE_BLOCKS = PAGES_TO_DISPLAY + 2
|
|||
export const buildPagedList = (
|
||||
numPages: number,
|
||||
activePage: number,
|
||||
): (string | number)[] => {
|
||||
): ("left" | "right" | number)[] => {
|
||||
if (numPages > NUM_PAGE_BLOCKS) {
|
||||
let pages = []
|
||||
const leftBound = activePage - PAGE_NEIGHBORS
|
||||
|
@ -44,8 +44,8 @@ export const buildPagedList = (
|
|||
const singleSpillOffset = PAGES_TO_DISPLAY - pages.length - 1
|
||||
const hasLeftOverflow = startPage > 2
|
||||
const hasRightOverflow = endPage < beforeLastPage
|
||||
const leftOverflowPage = "left"
|
||||
const rightOverflowPage = "right"
|
||||
const leftOverflowPage = "left" as const
|
||||
const rightOverflowPage = "right" as const
|
||||
|
||||
if (hasLeftOverflow && !hasRightOverflow) {
|
||||
const extraPages = range(startPage - singleSpillOffset, startPage - 1)
|
||||
|
|
|
@ -2,24 +2,22 @@ import TableCell from "@material-ui/core/TableCell"
|
|||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
|
||||
import { useActor } from "@xstate/react"
|
||||
import { AvatarData } from "components/AvatarData/AvatarData"
|
||||
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
|
||||
import { useClickable } from "hooks/useClickable"
|
||||
import { FC } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { getDisplayWorkspaceTemplateName } from "util/workspace"
|
||||
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
|
||||
import { LastUsed } from "../LastUsed/LastUsed"
|
||||
import { OutdatedHelpTooltip } from "../Tooltips"
|
||||
import { Workspace } from "api/typesGenerated"
|
||||
import { OutdatedHelpTooltip } from "components/Tooltips/OutdatedHelpTooltip"
|
||||
|
||||
export const WorkspacesRow: FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({
|
||||
workspaceRef,
|
||||
}) => {
|
||||
export const WorkspacesRow: FC<{
|
||||
workspace: Workspace
|
||||
onUpdateWorkspace: (workspace: Workspace) => void
|
||||
}> = ({ workspace, onUpdateWorkspace }) => {
|
||||
const styles = useStyles()
|
||||
const navigate = useNavigate()
|
||||
const [workspaceState, send] = useActor(workspaceRef)
|
||||
const { data: workspace } = workspaceState.context
|
||||
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`
|
||||
const hasTemplateIcon =
|
||||
workspace.template_icon && workspace.template_icon !== ""
|
||||
|
@ -58,7 +56,7 @@ export const WorkspacesRow: FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({
|
|||
{workspace.outdated && (
|
||||
<OutdatedHelpTooltip
|
||||
onUpdateVersion={() => {
|
||||
send("UPDATE_VERSION")
|
||||
onUpdateWorkspace(workspace)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -4,8 +4,8 @@ import TableCell from "@material-ui/core/TableCell"
|
|||
import TableContainer from "@material-ui/core/TableContainer"
|
||||
import TableHead from "@material-ui/core/TableHead"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import { Workspace } from "api/typesGenerated"
|
||||
import { FC } from "react"
|
||||
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
|
||||
import { WorkspacesTableBody } from "./WorkspacesTableBody"
|
||||
|
||||
const Language = {
|
||||
|
@ -18,15 +18,16 @@ const Language = {
|
|||
}
|
||||
|
||||
export interface WorkspacesTableProps {
|
||||
isLoading?: boolean
|
||||
workspaceRefs?: WorkspaceItemMachineRef[]
|
||||
filter?: string
|
||||
isNonInitialPage: boolean
|
||||
workspaces?: Workspace[]
|
||||
isUsingFilter: boolean
|
||||
onUpdateWorkspace: (workspace: Workspace) => void
|
||||
}
|
||||
|
||||
export const WorkspacesTable: FC<
|
||||
React.PropsWithChildren<WorkspacesTableProps>
|
||||
> = ({ isLoading, workspaceRefs, filter, isNonInitialPage }) => {
|
||||
export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
workspaces,
|
||||
isUsingFilter,
|
||||
onUpdateWorkspace,
|
||||
}) => {
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
|
@ -42,10 +43,9 @@ export const WorkspacesTable: FC<
|
|||
</TableHead>
|
||||
<TableBody>
|
||||
<WorkspacesTableBody
|
||||
isLoading={isLoading}
|
||||
workspaceRefs={workspaceRefs}
|
||||
filter={filter}
|
||||
isNonInitialPage={isNonInitialPage}
|
||||
workspaces={workspaces}
|
||||
isUsingFilter={isUsingFilter}
|
||||
onUpdateWorkspace={onUpdateWorkspace}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
|
|
@ -1,98 +1,79 @@
|
|||
import Button from "@material-ui/core/Button"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TableCell from "@material-ui/core/TableCell"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import AddOutlined from "@material-ui/icons/AddOutlined"
|
||||
import { Workspace } from "api/typesGenerated"
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||
import { TableEmpty } from "components/TableEmpty/TableEmpty"
|
||||
import { FC } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link as RouterLink } from "react-router-dom"
|
||||
import { workspaceFilterQuery } from "../../util/filters"
|
||||
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
|
||||
import { EmptyState } from "../EmptyState/EmptyState"
|
||||
import { TableLoader } from "../TableLoader/TableLoader"
|
||||
import { WorkspacesRow } from "./WorkspacesRow"
|
||||
|
||||
interface TableBodyProps {
|
||||
isLoading?: boolean
|
||||
workspaceRefs?: WorkspaceItemMachineRef[]
|
||||
filter?: string
|
||||
isNonInitialPage: boolean
|
||||
workspaces?: Workspace[]
|
||||
isUsingFilter: boolean
|
||||
onUpdateWorkspace: (workspace: Workspace) => void
|
||||
}
|
||||
|
||||
export const WorkspacesTableBody: FC<
|
||||
React.PropsWithChildren<TableBodyProps>
|
||||
> = ({ isLoading, workspaceRefs, filter, isNonInitialPage }) => {
|
||||
> = ({ workspaces, isUsingFilter, onUpdateWorkspace }) => {
|
||||
const { t } = useTranslation("workspacesPage")
|
||||
const styles = useStyles()
|
||||
|
||||
if (!workspaces) {
|
||||
return <TableLoader />
|
||||
}
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
return (
|
||||
<ChooseOne>
|
||||
<Cond condition={isUsingFilter}>
|
||||
<TableEmpty message={t("emptyResultsMessage")} />
|
||||
</Cond>
|
||||
|
||||
<Cond>
|
||||
<TableEmpty
|
||||
className={styles.withImage}
|
||||
message={t("emptyCreateWorkspaceMessage")}
|
||||
description={t("emptyCreateWorkspaceDescription")}
|
||||
cta={
|
||||
<Link underline="none" component={RouterLink} to="/templates">
|
||||
<Button startIcon={<AddOutlined />}>
|
||||
{t("createFromTemplateButton")}
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
image={
|
||||
<div className={styles.emptyImage}>
|
||||
<img src="/featured/workspaces.webp" alt="" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChooseOne>
|
||||
<Cond condition={Boolean(isLoading)}>
|
||||
<TableLoader />
|
||||
</Cond>
|
||||
<Cond condition={!workspaceRefs || workspaceRefs.length === 0}>
|
||||
<TableRow>
|
||||
<TableCell colSpan={999} className={styles.emptyTableCell}>
|
||||
<ChooseOne>
|
||||
<Cond condition={isNonInitialPage}>
|
||||
<EmptyState message={t("emptyPageMessage")} />
|
||||
</Cond>
|
||||
<Cond
|
||||
condition={
|
||||
filter === workspaceFilterQuery.me ||
|
||||
filter === workspaceFilterQuery.all
|
||||
}
|
||||
>
|
||||
<EmptyState
|
||||
className={styles.empty}
|
||||
message={t("emptyCreateWorkspaceMessage")}
|
||||
description={t("emptyCreateWorkspaceDescription")}
|
||||
cta={
|
||||
<Link
|
||||
underline="none"
|
||||
component={RouterLink}
|
||||
to="/templates"
|
||||
>
|
||||
<Button startIcon={<AddOutlined />}>
|
||||
{t("createFromTemplateButton")}
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
image={
|
||||
<div className={styles.emptyImage}>
|
||||
<img src="/featured/workspaces.webp" alt="" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Cond>
|
||||
<Cond>
|
||||
<EmptyState message={t("emptyResultsMessage")} />
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Cond>
|
||||
<Cond>
|
||||
{workspaceRefs &&
|
||||
workspaceRefs.map((workspaceRef) => (
|
||||
<WorkspacesRow workspaceRef={workspaceRef} key={workspaceRef.id} />
|
||||
))}
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
<>
|
||||
{workspaces.map((workspace) => (
|
||||
<WorkspacesRow
|
||||
workspace={workspace}
|
||||
key={workspace.id}
|
||||
onUpdateWorkspace={onUpdateWorkspace}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
emptyTableCell: {
|
||||
padding: "0 !important",
|
||||
},
|
||||
|
||||
empty: {
|
||||
withImage: {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
|
||||
emptyImage: {
|
||||
maxWidth: "50%",
|
||||
height: theme.spacing(34),
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { useSearchParams } from "react-router-dom"
|
||||
|
||||
type UseFilterResult = {
|
||||
query: string
|
||||
setFilter: (query: string) => void
|
||||
}
|
||||
|
||||
export const useFilter = (defaultValue: string): UseFilterResult => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const query = searchParams.get("filter") ?? defaultValue
|
||||
|
||||
const setFilter = (query: string) => {
|
||||
searchParams.set("filter", query)
|
||||
setSearchParams(searchParams)
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
setFilter,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
|
||||
type UsePaginationResult = {
|
||||
page: number
|
||||
limit: number
|
||||
goToPage: (page: number) => void
|
||||
}
|
||||
|
||||
export const usePagination = (): UsePaginationResult => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 0
|
||||
const limit = DEFAULT_RECORDS_PER_PAGE
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
searchParams.set("page", page.toString())
|
||||
setSearchParams(searchParams)
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
goToPage,
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"emptyCreateWorkspaceMessage": "Create your first workspace",
|
||||
"emptyCreateWorkspaceDescription": "Start editing your source code and building your software.",
|
||||
"emptyCreateWorkspaceDescription": "Start editing your source code and building your software",
|
||||
"createFromTemplateButton": "Create from template",
|
||||
"emptyResultsMessage": "No results matched your search",
|
||||
"emptyPageMessage": "No results on this page"
|
||||
"emptyPageMessage": "No results on this page",
|
||||
"updateVersionError": "Error on update workspace version"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { fireEvent, screen, waitFor } from "@testing-library/react"
|
||||
import * as API from "../../../api/api"
|
||||
import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackbar"
|
||||
import * as AccountForm from "../../../components/SettingsAccountForm/SettingsAccountForm"
|
||||
import { renderWithAuth } from "../../../testHelpers/renderHelpers"
|
||||
import * as AuthXService from "../../../xServices/auth/authXService"
|
||||
|
@ -10,12 +9,7 @@ import i18next from "i18next"
|
|||
const { t } = i18next
|
||||
|
||||
const renderPage = () => {
|
||||
return renderWithAuth(
|
||||
<>
|
||||
<AccountPage />
|
||||
<GlobalSnackbar />
|
||||
</>,
|
||||
)
|
||||
return renderWithAuth(<AccountPage />)
|
||||
}
|
||||
|
||||
const newData = {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { fireEvent, screen, within } from "@testing-library/react"
|
||||
import * as API from "../../../api/api"
|
||||
import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackbar"
|
||||
import {
|
||||
MockGitSSHKey,
|
||||
renderWithAuth,
|
||||
|
@ -20,12 +19,7 @@ describe("SSH keys Page", () => {
|
|||
describe("regenerate SSH key", () => {
|
||||
describe("when it is success", () => {
|
||||
it("shows a success message and updates the ssh key on the page", async () => {
|
||||
renderWithAuth(
|
||||
<>
|
||||
<SSHKeysPage />
|
||||
<GlobalSnackbar />
|
||||
</>,
|
||||
)
|
||||
renderWithAuth(<SSHKeysPage />)
|
||||
|
||||
// Wait to the ssh be rendered on the screen
|
||||
await screen.findByText(MockGitSSHKey.public_key)
|
||||
|
@ -69,12 +63,7 @@ describe("SSH keys Page", () => {
|
|||
|
||||
describe("when it fails", () => {
|
||||
it("shows an error message", async () => {
|
||||
renderWithAuth(
|
||||
<>
|
||||
<SSHKeysPage />
|
||||
<GlobalSnackbar />
|
||||
</>,
|
||||
)
|
||||
renderWithAuth(<SSHKeysPage />)
|
||||
|
||||
// Wait to the ssh be rendered on the screen
|
||||
await screen.findByText(MockGitSSHKey.public_key)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { fireEvent, screen, waitFor } from "@testing-library/react"
|
||||
import * as API from "../../../api/api"
|
||||
import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackbar"
|
||||
import * as SecurityForm from "../../../components/SettingsSecurityForm/SettingsSecurityForm"
|
||||
import { renderWithAuth } from "../../../testHelpers/renderHelpers"
|
||||
import { SecurityPage } from "./SecurityPage"
|
||||
|
@ -9,12 +8,7 @@ import i18next from "i18next"
|
|||
const { t } = i18next
|
||||
|
||||
const renderPage = () => {
|
||||
return renderWithAuth(
|
||||
<>
|
||||
<SecurityPage />
|
||||
<GlobalSnackbar />
|
||||
</>,
|
||||
)
|
||||
return renderWithAuth(<SecurityPage />)
|
||||
}
|
||||
|
||||
const newData = {
|
||||
|
|
|
@ -6,7 +6,6 @@ import { Language as usersXServiceLanguage } from "xServices/users/usersXService
|
|||
import * as API from "../../api/api"
|
||||
import { Role } from "../../api/typesGenerated"
|
||||
import { Language as ResetPasswordDialogLanguage } from "../../components/Dialogs/ResetPasswordDialog/ResetPasswordDialog"
|
||||
import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar"
|
||||
import {
|
||||
MockAuditorRole,
|
||||
MockOwnerRole,
|
||||
|
@ -21,12 +20,7 @@ import { Language as UsersPageLanguage, UsersPage } from "./UsersPage"
|
|||
const { t } = i18n
|
||||
|
||||
const renderPage = () => {
|
||||
return renderWithAuth(
|
||||
<>
|
||||
<UsersPage />
|
||||
<GlobalSnackbar />
|
||||
</>,
|
||||
)
|
||||
return renderWithAuth(<UsersPage />)
|
||||
}
|
||||
|
||||
const suspendUser = async (setupActionSpies: () => void) => {
|
||||
|
|
|
@ -1,35 +1,20 @@
|
|||
import { useMachine } from "@xstate/react"
|
||||
import {
|
||||
getPaginationContext,
|
||||
nonInitialPage,
|
||||
} from "components/PaginationWidget/utils"
|
||||
import { useFilter } from "hooks/useFilter"
|
||||
import { usePagination } from "hooks/usePagination"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
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 { useWorkspacesData, useWorkspaceUpdate } from "./data"
|
||||
import { WorkspacesPageView } from "./WorkspacesPageView"
|
||||
|
||||
const WorkspacesPage: FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const filter = searchParams.get("filter") ?? workspaceFilterQuery.me
|
||||
const [workspacesState, send] = useMachine(workspacesMachine, {
|
||||
context: {
|
||||
filter,
|
||||
paginationContext: getPaginationContext(searchParams),
|
||||
},
|
||||
actions: {
|
||||
// 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 filter = useFilter(workspaceFilterQuery.me)
|
||||
const pagination = usePagination()
|
||||
const { data, error, queryKey } = useWorkspacesData({
|
||||
...pagination,
|
||||
...filter,
|
||||
})
|
||||
|
||||
const { workspaceRefs, count, getWorkspacesError } = workspacesState.context
|
||||
const paginationRef = workspacesState.context
|
||||
.paginationRef as PaginationMachineRef
|
||||
const updateWorkspace = useWorkspaceUpdate(queryKey)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -38,19 +23,17 @@ const WorkspacesPage: FC = () => {
|
|||
</Helmet>
|
||||
|
||||
<WorkspacesPageView
|
||||
filter={workspacesState.context.filter}
|
||||
isLoading={!workspaceRefs}
|
||||
workspaceRefs={workspaceRefs}
|
||||
count={count}
|
||||
getWorkspacesError={getWorkspacesError}
|
||||
onFilter={(query) => {
|
||||
send({
|
||||
type: "UPDATE_FILTER",
|
||||
query,
|
||||
})
|
||||
workspaces={data?.workspaces}
|
||||
error={error}
|
||||
filter={filter.query}
|
||||
onFilter={filter.setFilter}
|
||||
count={data?.count}
|
||||
page={pagination.page}
|
||||
limit={pagination.limit}
|
||||
onPageChange={pagination.goToPage}
|
||||
onUpdateWorkspace={(workspace) => {
|
||||
updateWorkspace.mutate(workspace)
|
||||
}}
|
||||
paginationRef={paginationRef}
|
||||
isNonInitialPage={nonInitialPage(searchParams)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,102 +1,66 @@
|
|||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { createPaginationRef } from "components/PaginationWidget/utils"
|
||||
import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"
|
||||
import dayjs from "dayjs"
|
||||
import { spawn } from "xstate"
|
||||
import uniqueId from "lodash/uniqueId"
|
||||
import {
|
||||
ProvisionerJobStatus,
|
||||
WorkspaceTransition,
|
||||
Workspace,
|
||||
WorkspaceStatus,
|
||||
WorkspaceStatuses,
|
||||
} from "../../api/typesGenerated"
|
||||
import { MockWorkspace } from "../../testHelpers/entities"
|
||||
import { workspaceFilterQuery } from "../../util/filters"
|
||||
import {
|
||||
workspaceItemMachine,
|
||||
WorkspaceItemMachineRef,
|
||||
} from "../../xServices/workspaces/workspacesXService"
|
||||
import {
|
||||
WorkspacesPageView,
|
||||
WorkspacesPageViewProps,
|
||||
} from "./WorkspacesPageView"
|
||||
|
||||
const createWorkspaceItemRef = (
|
||||
status: ProvisionerJobStatus,
|
||||
transition: WorkspaceTransition = "start",
|
||||
const createWorkspace = (
|
||||
status: WorkspaceStatus,
|
||||
outdated = false,
|
||||
lastUsedAt = "0001-01-01",
|
||||
): WorkspaceItemMachineRef => {
|
||||
return spawn(
|
||||
workspaceItemMachine.withContext({
|
||||
data: {
|
||||
...MockWorkspace,
|
||||
outdated,
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
transition,
|
||||
job: {
|
||||
...MockWorkspace.latest_build.job,
|
||||
status: status,
|
||||
},
|
||||
},
|
||||
last_used_at: lastUsedAt,
|
||||
},
|
||||
}),
|
||||
)
|
||||
): Workspace => {
|
||||
return {
|
||||
...MockWorkspace,
|
||||
id: uniqueId("workspace"),
|
||||
outdated,
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
status,
|
||||
},
|
||||
last_used_at: lastUsedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// This is type restricted to prevent future statuses from slipping
|
||||
// through the cracks unchecked!
|
||||
const workspaces: { [key in ProvisionerJobStatus]: WorkspaceItemMachineRef } = {
|
||||
canceled: createWorkspaceItemRef("canceled"),
|
||||
canceling: createWorkspaceItemRef("canceling"),
|
||||
failed: createWorkspaceItemRef("failed"),
|
||||
pending: createWorkspaceItemRef("pending"),
|
||||
running: createWorkspaceItemRef("running"),
|
||||
succeeded: createWorkspaceItemRef("succeeded"),
|
||||
}
|
||||
const workspaces = WorkspaceStatuses.map((status) => createWorkspace(status))
|
||||
|
||||
const additionalWorkspaces: Record<string, WorkspaceItemMachineRef> = {
|
||||
runningAndStop: createWorkspaceItemRef("running", "stop"),
|
||||
succeededAndStop: createWorkspaceItemRef("succeeded", "stop"),
|
||||
runningAndDelete: createWorkspaceItemRef("running", "delete"),
|
||||
outdated: createWorkspaceItemRef("running", "delete", true),
|
||||
active: createWorkspaceItemRef(
|
||||
// Additional Workspaces depending on time
|
||||
const additionalWorkspaces: Record<string, Workspace> = {
|
||||
today: createWorkspace(
|
||||
"running",
|
||||
undefined,
|
||||
true,
|
||||
dayjs().toString(),
|
||||
),
|
||||
today: createWorkspaceItemRef(
|
||||
"running",
|
||||
undefined,
|
||||
true,
|
||||
dayjs().subtract(3, "hour").toString(),
|
||||
),
|
||||
old: createWorkspaceItemRef(
|
||||
old: createWorkspace("running", true, dayjs().subtract(1, "week").toString()),
|
||||
veryOld: createWorkspace(
|
||||
"running",
|
||||
undefined,
|
||||
true,
|
||||
dayjs().subtract(1, "week").toString(),
|
||||
),
|
||||
veryOld: createWorkspaceItemRef(
|
||||
"running",
|
||||
undefined,
|
||||
true,
|
||||
dayjs().subtract(1, "month").subtract(4, "day").toString(),
|
||||
),
|
||||
}
|
||||
|
||||
const allWorkspaces = [
|
||||
...Object.values(workspaces),
|
||||
...Object.values(additionalWorkspaces),
|
||||
]
|
||||
|
||||
export default {
|
||||
title: "pages/WorkspacesPageView",
|
||||
component: WorkspacesPageView,
|
||||
argTypes: {
|
||||
paginationRef: {
|
||||
defaultValue: createPaginationRef({ page: 1, limit: 25 }),
|
||||
},
|
||||
workspaceRefs: {
|
||||
options: [
|
||||
...Object.keys(workspaces),
|
||||
...Object.keys(additionalWorkspaces),
|
||||
],
|
||||
mapping: { ...workspaces, ...additionalWorkspaces },
|
||||
limit: {
|
||||
defaultValue: DEFAULT_RECORDS_PER_PAGE,
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof WorkspacesPageView>
|
||||
|
@ -107,34 +71,20 @@ const Template: Story<WorkspacesPageViewProps> = (args) => (
|
|||
|
||||
export const AllStates = Template.bind({})
|
||||
AllStates.args = {
|
||||
workspaceRefs: [
|
||||
...Object.values(workspaces),
|
||||
...Object.values(additionalWorkspaces),
|
||||
],
|
||||
count: 14,
|
||||
isNonInitialPage: false,
|
||||
workspaces: allWorkspaces,
|
||||
count: allWorkspaces.length,
|
||||
}
|
||||
|
||||
export const OwnerHasNoWorkspaces = Template.bind({})
|
||||
OwnerHasNoWorkspaces.args = {
|
||||
workspaceRefs: [],
|
||||
workspaces: [],
|
||||
filter: workspaceFilterQuery.me,
|
||||
count: 0,
|
||||
isNonInitialPage: false,
|
||||
}
|
||||
|
||||
export const NoResults = Template.bind({})
|
||||
NoResults.args = {
|
||||
workspaceRefs: [],
|
||||
export const NoSearchResults = Template.bind({})
|
||||
NoSearchResults.args = {
|
||||
workspaces: [],
|
||||
filter: "searchtearmwithnoresults",
|
||||
count: 0,
|
||||
isNonInitialPage: false,
|
||||
}
|
||||
|
||||
export const EmptyPage = Template.bind({})
|
||||
EmptyPage.args = {
|
||||
workspaceRefs: [],
|
||||
filter: workspaceFilterQuery.me,
|
||||
count: 0,
|
||||
isNonInitialPage: true,
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import Link from "@material-ui/core/Link"
|
||||
import { Workspace } from "api/typesGenerated"
|
||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||
import { Maybe } from "components/Conditionals/Maybe"
|
||||
import { PaginationWidget } from "components/PaginationWidget/PaginationWidget"
|
||||
import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase"
|
||||
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,
|
||||
|
@ -16,7 +16,6 @@ import { Stack } from "../../components/Stack/Stack"
|
|||
import { WorkspaceHelpTooltip } from "../../components/Tooltips"
|
||||
import { WorkspacesTable } from "../../components/WorkspacesTable/WorkspacesTable"
|
||||
import { workspaceFilterQuery } from "../../util/filters"
|
||||
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
|
||||
|
||||
export const Language = {
|
||||
pageTitle: "Workspaces",
|
||||
|
@ -28,27 +27,29 @@ export const Language = {
|
|||
}
|
||||
|
||||
export interface WorkspacesPageViewProps {
|
||||
isLoading?: boolean
|
||||
workspaceRefs?: WorkspaceItemMachineRef[]
|
||||
error: unknown
|
||||
workspaces?: Workspace[]
|
||||
count?: number
|
||||
getWorkspacesError: Error | unknown
|
||||
filter?: string
|
||||
page: number
|
||||
limit: number
|
||||
filter: string
|
||||
onPageChange: (page: number) => void
|
||||
onFilter: (query: string) => void
|
||||
paginationRef: PaginationMachineRef
|
||||
isNonInitialPage: boolean
|
||||
onUpdateWorkspace: (workspace: Workspace) => void
|
||||
}
|
||||
|
||||
export const WorkspacesPageView: FC<
|
||||
React.PropsWithChildren<WorkspacesPageViewProps>
|
||||
> = ({
|
||||
isLoading,
|
||||
workspaceRefs,
|
||||
count,
|
||||
getWorkspacesError,
|
||||
workspaces,
|
||||
error,
|
||||
filter,
|
||||
page,
|
||||
limit,
|
||||
count,
|
||||
onFilter,
|
||||
paginationRef,
|
||||
isNonInitialPage,
|
||||
onPageChange,
|
||||
onUpdateWorkspace,
|
||||
}) => {
|
||||
const presetFilters = [
|
||||
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
|
||||
|
@ -79,11 +80,11 @@ export const WorkspacesPageView: FC<
|
|||
</PageHeader>
|
||||
|
||||
<Stack>
|
||||
<Maybe condition={getWorkspacesError !== undefined}>
|
||||
<Maybe condition={Boolean(error)}>
|
||||
<AlertBanner
|
||||
error={getWorkspacesError}
|
||||
error={error}
|
||||
severity={
|
||||
workspaceRefs !== undefined && workspaceRefs.length > 0
|
||||
workspaces !== undefined && workspaces.length > 0
|
||||
? "warning"
|
||||
: "error"
|
||||
}
|
||||
|
@ -96,15 +97,19 @@ export const WorkspacesPageView: FC<
|
|||
presetFilters={presetFilters}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<WorkspacesTable
|
||||
isLoading={isLoading}
|
||||
workspaceRefs={workspaceRefs}
|
||||
filter={filter}
|
||||
isNonInitialPage={isNonInitialPage}
|
||||
workspaces={workspaces}
|
||||
isUsingFilter={filter !== workspaceFilterQuery.me}
|
||||
onUpdateWorkspace={onUpdateWorkspace}
|
||||
/>
|
||||
|
||||
<PaginationWidget numRecords={count} paginationRef={paginationRef} />
|
||||
{count !== undefined && (
|
||||
<PaginationWidgetBase
|
||||
count={count}
|
||||
limit={limit}
|
||||
onChange={onPageChange}
|
||||
page={page}
|
||||
/>
|
||||
)}
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
import {
|
||||
QueryKey,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query"
|
||||
import { getWorkspaces, updateWorkspaceVersion } from "api/api"
|
||||
import { getErrorMessage } from "api/errors"
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceBuild,
|
||||
WorkspacesResponse,
|
||||
} from "api/typesGenerated"
|
||||
import { displayError } from "components/GlobalSnackbar/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
type UseWorkspacesDataParams = {
|
||||
page: number
|
||||
limit: number
|
||||
query: string
|
||||
}
|
||||
|
||||
export const useWorkspacesData = ({
|
||||
page,
|
||||
limit,
|
||||
query,
|
||||
}: UseWorkspacesDataParams) => {
|
||||
const queryKey = ["workspaces", query, page]
|
||||
const result = useQuery({
|
||||
queryKey,
|
||||
queryFn: () =>
|
||||
getWorkspaces({
|
||||
q: query,
|
||||
limit: limit,
|
||||
offset: page,
|
||||
}),
|
||||
refetchInterval: 5_000,
|
||||
})
|
||||
|
||||
return {
|
||||
...result,
|
||||
queryKey,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkspaceUpdate = (queryKey: QueryKey) => {
|
||||
const queryClient = useQueryClient()
|
||||
const { t } = useTranslation("workspacesPage")
|
||||
|
||||
return useMutation({
|
||||
mutationFn: updateWorkspaceVersion,
|
||||
onMutate: async (workspace) => {
|
||||
await queryClient.cancelQueries({ queryKey })
|
||||
queryClient.setQueryData<WorkspacesResponse>(queryKey, (oldResponse) => {
|
||||
if (oldResponse) {
|
||||
return assignPendingStatus(oldResponse, workspace)
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: (workspaceBuild) => {
|
||||
queryClient.setQueryData<WorkspacesResponse>(queryKey, (oldResponse) => {
|
||||
if (oldResponse) {
|
||||
return assignLatestBuild(oldResponse, workspaceBuild)
|
||||
}
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = getErrorMessage(error, t("updateVersionError"))
|
||||
displayError(message)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const assignLatestBuild = (
|
||||
oldResponse: WorkspacesResponse,
|
||||
build: WorkspaceBuild,
|
||||
): WorkspacesResponse => {
|
||||
return {
|
||||
...oldResponse,
|
||||
workspaces: oldResponse.workspaces.map((workspace) => {
|
||||
if (workspace.id === build.workspace_id) {
|
||||
return {
|
||||
...workspace,
|
||||
latest_build: build,
|
||||
}
|
||||
}
|
||||
|
||||
return workspace
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const assignPendingStatus = (
|
||||
oldResponse: WorkspacesResponse,
|
||||
workspace: Workspace,
|
||||
): WorkspacesResponse => {
|
||||
return {
|
||||
...oldResponse,
|
||||
workspaces: oldResponse.workspaces.map((workspaceItem) => {
|
||||
if (workspaceItem.id === workspace.id) {
|
||||
return {
|
||||
...workspace,
|
||||
latest_build: {
|
||||
...workspace.latest_build,
|
||||
status: "pending",
|
||||
job: {
|
||||
...workspace.latest_build.job,
|
||||
status: "pending",
|
||||
},
|
||||
},
|
||||
} as Workspace
|
||||
}
|
||||
|
||||
return workspace
|
||||
}),
|
||||
}
|
||||
}
|
|
@ -1,16 +1,14 @@
|
|||
import ThemeProvider from "@material-ui/styles/ThemeProvider"
|
||||
import {
|
||||
render as wrappedRender,
|
||||
RenderResult,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
} from "@testing-library/react"
|
||||
import { AuthProvider } from "components/AuthProvider/AuthProvider"
|
||||
import { AppProviders } from "app"
|
||||
import { DashboardLayout } from "components/Dashboard/DashboardLayout"
|
||||
import { createMemoryHistory } from "history"
|
||||
import { i18n } from "i18n"
|
||||
import { FC, ReactElement } from "react"
|
||||
import { HelmetProvider } from "react-helmet-async"
|
||||
import { I18nextProvider } from "react-i18next"
|
||||
import {
|
||||
MemoryRouter,
|
||||
|
@ -19,7 +17,6 @@ import {
|
|||
unstable_HistoryRouter as HistoryRouter,
|
||||
} from "react-router-dom"
|
||||
import { RequireAuth } from "../components/RequireAuth/RequireAuth"
|
||||
import { dark } from "../theme"
|
||||
import { MockUser } from "./entities"
|
||||
|
||||
export const history = createMemoryHistory()
|
||||
|
@ -28,13 +25,9 @@ export const WrapperComponent: FC<React.PropsWithChildren<unknown>> = ({
|
|||
children,
|
||||
}) => {
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<ThemeProvider theme={dark}>
|
||||
<HistoryRouter history={history}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</HistoryRouter>
|
||||
</ThemeProvider>
|
||||
</HelmetProvider>
|
||||
<AppProviders>
|
||||
<HistoryRouter history={history}>{children}</HistoryRouter>
|
||||
</AppProviders>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -59,24 +52,20 @@ export function renderWithAuth(
|
|||
}: { route?: string; path?: string; routes?: JSX.Element } = {},
|
||||
): RenderWithAuthResult {
|
||||
const renderResult = wrappedRender(
|
||||
<HelmetProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<ThemeProvider theme={dark}>
|
||||
<AuthProvider>
|
||||
<MemoryRouter initialEntries={[route]}>
|
||||
<Routes>
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route element={<DashboardLayout />}>
|
||||
<Route path={path ?? route} element={ui} />
|
||||
</Route>
|
||||
</Route>
|
||||
{routes}
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</I18nextProvider>
|
||||
</HelmetProvider>,
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<AppProviders>
|
||||
<MemoryRouter initialEntries={[route]}>
|
||||
<Routes>
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route element={<DashboardLayout />}>
|
||||
<Route path={path ?? route} element={ui} />
|
||||
</Route>
|
||||
</Route>
|
||||
{routes}
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</AppProviders>
|
||||
</I18nextProvider>,
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
|
@ -2897,6 +2897,19 @@
|
|||
regenerator-runtime "^0.13.7"
|
||||
resolve-from "^5.0.0"
|
||||
|
||||
"@tanstack/query-core@4.22.4":
|
||||
version "4.22.4"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.22.4.tgz#aca622d2f8800a147ece5520d956a076ab92f0ea"
|
||||
integrity sha512-t79CMwlbBnj+yL82tEcmRN93bL4U3pae2ota4t5NN2z3cIeWw74pzdWrKRwOfTvLcd+b30tC+ciDlfYOKFPGUw==
|
||||
|
||||
"@tanstack/react-query@4.22.4":
|
||||
version "4.22.4"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.22.4.tgz#851581c645f1c9cfcd394448fedd980a39bbc3fe"
|
||||
integrity sha512-e5j5Z88XUQGeEPMyz5XF1V0mMf6Da+6URXiTpZfUb9nuHs2nlNoA+EoIvnhccE5b9YT6Yg7kARhn2L7u94M/4A==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "4.22.4"
|
||||
use-sync-external-store "^1.2.0"
|
||||
|
||||
"@testing-library/dom@^8.5.0":
|
||||
version "8.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.19.0.tgz#bd3f83c217ebac16694329e413d9ad5fdcfd785f"
|
||||
|
@ -13804,7 +13817,7 @@ use-isomorphic-layout-effect@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
||||
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
||||
|
||||
use-sync-external-store@^1.0.0:
|
||||
use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
|
Loading…
Reference in New Issue