From f5918871f4c60072d3c7bf4f68486d075c4f5062 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 23 Jan 2023 18:30:29 +0000 Subject: [PATCH 01/10] chore: Use react-query --- site/package.json | 1 + site/src/api/api.ts | 7 + site/src/app.tsx | 20 ++- .../PaginationWidget/PaginationWidgetBase.tsx | 104 +++++++++++ site/src/components/PaginationWidget/utils.ts | 6 +- .../WorkspacesTable/WorkspacesRow.tsx | 16 +- .../WorkspacesTable/WorkspacesTable.tsx | 24 +-- .../WorkspacesTable/WorkspacesTableBody.tsx | 114 +++++------- .../pages/WorkspacesPage/WorkspacesPage.tsx | 165 ++++++++++++++---- .../WorkspacesPage/WorkspacesPageView.tsx | 47 +++-- site/yarn.lock | 15 +- 11 files changed, 360 insertions(+), 159 deletions(-) create mode 100644 site/src/components/PaginationWidget/PaginationWidgetBase.tsx diff --git a/site/package.json b/site/package.json index e0cd2d23f42ef..91a4ec6abd155 100644 --- a/site/package.json +++ b/site/package.json @@ -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", diff --git a/site/src/api/api.ts b/site/src/api/api.ts index a02683bece396..885e701a644b3 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -779,3 +779,10 @@ export const getTemplateVersionLogs = async ( ) return response.data } + +export const updateWorkspaceVersion = async ( + workspace: TypesGen.Workspace, +): Promise => { + const template = await getTemplate(workspace.template_id) + return startWorkspace(workspace.id, template.active_version_id) +} diff --git a/site/src/app.tsx b/site/src/app.tsx index 5dbc2d5e7fcd5..5e40bc4afac53 100644 --- a/site/src/app.tsx +++ b/site/src/app.tsx @@ -1,5 +1,6 @@ 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 { HelmetProvider } from "react-helmet-async" @@ -9,16 +10,27 @@ import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar" import { dark } from "./theme" import "./theme/globalFonts" +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + }, +}) + export const App: FC = () => { return ( - - - - + + + + + + diff --git a/site/src/components/PaginationWidget/PaginationWidgetBase.tsx b/site/src/components/PaginationWidget/PaginationWidgetBase.tsx new file mode 100644 index 0000000000000..b145b8ec1a1ff --- /dev/null +++ b/site/src/components/PaginationWidget/PaginationWidgetBase.tsx @@ -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 === 1) { + return null + } + + return ( +
+ + + + + + + {buildPagedList(numPages, page).map((pageItem) => { + if (pageItem === "left" || pageItem === "right") { + return ( + + ) + } + + return ( + onChange(pageItem)} + /> + ) + })} + + + +
+ ) +} + +const useStyles = makeStyles((theme) => ({ + defaultContainerStyles: { + justifyContent: "center", + alignItems: "center", + display: "flex", + flexDirection: "row", + padding: "20px", + }, + + prevLabelStyles: { + marginRight: `${theme.spacing(0.5)}px`, + }, +})) diff --git a/site/src/components/PaginationWidget/utils.ts b/site/src/components/PaginationWidget/utils.ts index a2cd41880ef2c..db25da9b15f8e 100644 --- a/site/src/components/PaginationWidget/utils.ts +++ b/site/src/components/PaginationWidget/utils.ts @@ -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) diff --git a/site/src/components/WorkspacesTable/WorkspacesRow.tsx b/site/src/components/WorkspacesTable/WorkspacesRow.tsx index 366f1f64f1e77..d2fa9960eb85a 100644 --- a/site/src/components/WorkspacesTable/WorkspacesRow.tsx +++ b/site/src/components/WorkspacesTable/WorkspacesRow.tsx @@ -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 && ( { - send("UPDATE_VERSION") + onUpdateWorkspace(workspace) }} /> )} diff --git a/site/src/components/WorkspacesTable/WorkspacesTable.tsx b/site/src/components/WorkspacesTable/WorkspacesTable.tsx index b20ef63b7b46f..5ae9ccc8a5784 100644 --- a/site/src/components/WorkspacesTable/WorkspacesTable.tsx +++ b/site/src/components/WorkspacesTable/WorkspacesTable.tsx @@ -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 -> = ({ isLoading, workspaceRefs, filter, isNonInitialPage }) => { +export const WorkspacesTable: FC = ({ + workspaces, + isUsingFilter, + onUpdateWorkspace, +}) => { return ( @@ -42,10 +43,9 @@ export const WorkspacesTable: FC<
diff --git a/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx b/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx index db41fb3bc078b..1af5a9c67e977 100644 --- a/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx +++ b/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx @@ -1,98 +1,68 @@ 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 { ChooseOne, Cond } from "components/Conditionals/ChooseOne" +import { Workspace } from "api/typesGenerated" +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 -> = ({ isLoading, workspaceRefs, filter, isNonInitialPage }) => { +> = ({ workspaces, isUsingFilter, onUpdateWorkspace }) => { const { t } = useTranslation("workspacesPage") const styles = useStyles() + if (!workspaces) { + return + } + + if (workspaces.length === 0) { + return isUsingFilter ? ( + + ) : ( + + + + } + image={ +
+ +
+ } + /> + ) + } + return ( - - - - - - - - - - - - - - - - } - image={ -
- -
- } - /> -
- - - -
-
-
-
- - {workspaceRefs && - workspaceRefs.map((workspaceRef) => ( - - ))} - -
+ <> + {workspaces.map((workspace) => ( + + ))} + ) } const useStyles = makeStyles((theme) => ({ - emptyTableCell: { - padding: "0 !important", - }, - - empty: { - paddingBottom: 0, - }, - emptyImage: { maxWidth: "50%", height: theme.spacing(34), diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 6a9e1197ac58d..8436a261d325b 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,36 +1,135 @@ -import { useMachine } from "@xstate/react" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { getWorkspaces, updateWorkspaceVersion } from "api/api" import { - getPaginationContext, - nonInitialPage, -} from "components/PaginationWidget/utils" + Workspace, + WorkspaceBuild, + WorkspacesResponse, +} from "api/typesGenerated" +import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils" 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 { WorkspacesPageView } from "./WorkspacesPageView" -const WorkspacesPage: FC = () => { +const usePagination = () => { + 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, + } +} + +const useFilter = () => { const [searchParams, setSearchParams] = useSearchParams() - const filter = searchParams.get("filter") ?? workspaceFilterQuery.me - const [workspacesState, send] = useMachine(workspacesMachine, { - context: { - filter, - paginationContext: getPaginationContext(searchParams), + const query = searchParams.get("filter") ?? workspaceFilterQuery.me + + const setFilter = (query: string) => { + searchParams.set("filter", query) + setSearchParams(searchParams) + } + + return { + query, + setFilter, + } +} + +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 + }), + } +} + +const WorkspacesPage: FC = () => { + const filter = useFilter() + const pagination = usePagination() + const queryClient = useQueryClient() + const workspacesQueryKey = ["workspaces", filter.query, pagination.page] + const { data, error } = useQuery({ + queryKey: workspacesQueryKey, + queryFn: () => + getWorkspaces({ + q: filter.query, + limit: pagination.limit, + offset: pagination.page, + }), + refetchInterval: 5_000, + }) + const updateWorkspace = useMutation({ + mutationFn: updateWorkspaceVersion, + onMutate: async (workspace) => { + await queryClient.cancelQueries({ queryKey: workspacesQueryKey }) + queryClient.setQueryData( + workspacesQueryKey, + (oldResponse) => { + if (oldResponse) { + return assignPendingStatus(oldResponse, workspace) + } + }, + ) }, - 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 }), + onSuccess: (workspaceBuild) => { + queryClient.setQueryData( + workspacesQueryKey, + (oldResponse) => { + if (oldResponse) { + return assignLatestBuild(oldResponse, workspaceBuild) + } + }, + ) }, }) - const { workspaceRefs, count, getWorkspacesError } = workspacesState.context - const paginationRef = workspacesState.context - .paginationRef as PaginationMachineRef - return ( <> @@ -38,19 +137,21 @@ const WorkspacesPage: FC = () => { { - send({ - type: "UPDATE_FILTER", - query, - }) + workspaces={data?.workspaces} + error={error} + filter={filter.query} + onFilter={filter.setFilter} + pagination={ + data && { + limit: pagination.limit, + page: pagination.page, + count: data.count, + onChange: pagination.goToPage, + } + } + onUpdateWorkspace={(workspace) => { + updateWorkspace.mutate(workspace) }} - paginationRef={paginationRef} - isNonInitialPage={nonInitialPage(searchParams)} /> ) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 7532c3aa4d816..fcf5ae69cc732 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -1,10 +1,13 @@ 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, + PaginationWidgetBaseProps, +} 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 +19,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 +30,23 @@ export const Language = { } export interface WorkspacesPageViewProps { - isLoading?: boolean - workspaceRefs?: WorkspaceItemMachineRef[] - count?: number - getWorkspacesError: Error | unknown - filter?: string + error: unknown + workspaces?: Workspace[] + pagination?: PaginationWidgetBaseProps + filter: string onFilter: (query: string) => void - paginationRef: PaginationMachineRef - isNonInitialPage: boolean + onUpdateWorkspace: (workspace: Workspace) => void } export const WorkspacesPageView: FC< React.PropsWithChildren > = ({ - isLoading, - workspaceRefs, - count, - getWorkspacesError, + workspaces, + error, filter, onFilter, - paginationRef, - isNonInitialPage, + pagination, + onUpdateWorkspace, }) => { const presetFilters = [ { query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton }, @@ -79,11 +77,11 @@ export const WorkspacesPageView: FC< - + 0 + workspaces !== undefined && workspaces.length > 0 ? "warning" : "error" } @@ -96,15 +94,12 @@ export const WorkspacesPageView: FC< presetFilters={presetFilters} /> - - - + {pagination && } ) } diff --git a/site/yarn.lock b/site/yarn.lock index 07f5d5fb6d07e..deb7e5505e968 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -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== From 5ef84606ae024fb09b7c86335b6e158b72f542e2 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 24 Jan 2023 14:40:06 +0000 Subject: [PATCH 02/10] chore: Use react-query and refactor workspaces page --- site/.eslintrc.yaml | 1 - site/src/hooks/useFilter.ts | 21 +++ site/src/hooks/usePagination.ts | 25 +++ .../pages/WorkspacesPage/WorkspacesPage.tsx | 142 ++---------------- .../WorkspacesPageView.stories.tsx | 115 ++++---------- .../WorkspacesPage/WorkspacesPageView.tsx | 24 ++- site/src/pages/WorkspacesPage/data.ts | 113 ++++++++++++++ 7 files changed, 221 insertions(+), 220 deletions(-) create mode 100644 site/src/hooks/useFilter.ts create mode 100644 site/src/hooks/usePagination.ts create mode 100644 site/src/pages/WorkspacesPage/data.ts diff --git a/site/.eslintrc.yaml b/site/.eslintrc.yaml index c467e18627595..6be87c5c80f52 100644 --- a/site/.eslintrc.yaml +++ b/site/.eslintrc.yaml @@ -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 diff --git a/site/src/hooks/useFilter.ts b/site/src/hooks/useFilter.ts new file mode 100644 index 0000000000000..823c1a22d69d7 --- /dev/null +++ b/site/src/hooks/useFilter.ts @@ -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, + } +} diff --git a/site/src/hooks/usePagination.ts b/site/src/hooks/usePagination.ts new file mode 100644 index 0000000000000..22e2099718613 --- /dev/null +++ b/site/src/hooks/usePagination.ts @@ -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, + } +} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 8436a261d325b..33e894d03b4c9 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,134 +1,20 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { getWorkspaces, updateWorkspaceVersion } from "api/api" -import { - Workspace, - WorkspaceBuild, - WorkspacesResponse, -} from "api/typesGenerated" -import { DEFAULT_RECORDS_PER_PAGE } 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 { useWorkspacesData, useWorkspaceUpdate } from "./data" import { WorkspacesPageView } from "./WorkspacesPageView" -const usePagination = () => { - 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, - } -} - -const useFilter = () => { - const [searchParams, setSearchParams] = useSearchParams() - const query = searchParams.get("filter") ?? workspaceFilterQuery.me - - const setFilter = (query: string) => { - searchParams.set("filter", query) - setSearchParams(searchParams) - } - - return { - query, - setFilter, - } -} - -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 - }), - } -} - const WorkspacesPage: FC = () => { - const filter = useFilter() + const filter = useFilter(workspaceFilterQuery.me) const pagination = usePagination() - const queryClient = useQueryClient() - const workspacesQueryKey = ["workspaces", filter.query, pagination.page] - const { data, error } = useQuery({ - queryKey: workspacesQueryKey, - queryFn: () => - getWorkspaces({ - q: filter.query, - limit: pagination.limit, - offset: pagination.page, - }), - refetchInterval: 5_000, - }) - const updateWorkspace = useMutation({ - mutationFn: updateWorkspaceVersion, - onMutate: async (workspace) => { - await queryClient.cancelQueries({ queryKey: workspacesQueryKey }) - queryClient.setQueryData( - workspacesQueryKey, - (oldResponse) => { - if (oldResponse) { - return assignPendingStatus(oldResponse, workspace) - } - }, - ) - }, - onSuccess: (workspaceBuild) => { - queryClient.setQueryData( - workspacesQueryKey, - (oldResponse) => { - if (oldResponse) { - return assignLatestBuild(oldResponse, workspaceBuild) - } - }, - ) - }, + const { data, error, queryKey } = useWorkspacesData({ + ...pagination, + ...filter, }) + const updateWorkspace = useWorkspaceUpdate(queryKey) return ( <> @@ -141,14 +27,10 @@ const WorkspacesPage: FC = () => { error={error} filter={filter.query} onFilter={filter.setFilter} - pagination={ - data && { - limit: pagination.limit, - page: pagination.page, - count: data.count, - onChange: pagination.goToPage, - } - } + count={data?.count} + page={pagination.page} + limit={pagination.limit} + onPageChange={pagination.goToPage} onUpdateWorkspace={(workspace) => { updateWorkspace.mutate(workspace) }} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 18dbc66405a51..fc8060328c679 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -1,104 +1,62 @@ import { ComponentMeta, Story } from "@storybook/react" -import { createPaginationRef } 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 = { - runningAndStop: createWorkspaceItemRef("running", "stop"), - succeededAndStop: createWorkspaceItemRef("succeeded", "stop"), - runningAndDelete: createWorkspaceItemRef("running", "delete"), - outdated: createWorkspaceItemRef("running", "delete", true), - active: createWorkspaceItemRef( - "running", - undefined, - true, - dayjs().toString(), - ), - today: createWorkspaceItemRef( +// Additional Workspaces depending on time +const additionalWorkspaces: Record = { + today: createWorkspace( "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 }, - }, - }, } as ComponentMeta const Template: Story = (args) => ( @@ -107,34 +65,27 @@ const Template: Story = (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: [], + workspaces: [], filter: "searchtearmwithnoresults", count: 0, - isNonInitialPage: false, } export const EmptyPage = Template.bind({}) EmptyPage.args = { - workspaceRefs: [], + workspaces: [], filter: workspaceFilterQuery.me, count: 0, - isNonInitialPage: true, } diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index fcf5ae69cc732..eb5c08afefd27 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -2,10 +2,7 @@ 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 { - PaginationWidgetBase, - PaginationWidgetBaseProps, -} from "components/PaginationWidget/PaginationWidgetBase" +import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase" import { FC } from "react" import { Link as RouterLink } from "react-router-dom" import { Margins } from "../../components/Margins/Margins" @@ -32,8 +29,11 @@ export const Language = { export interface WorkspacesPageViewProps { error: unknown workspaces?: Workspace[] - pagination?: PaginationWidgetBaseProps + count?: number + page: number + limit: number filter: string + onPageChange: (page: number) => void onFilter: (query: string) => void onUpdateWorkspace: (workspace: Workspace) => void } @@ -44,8 +44,11 @@ export const WorkspacesPageView: FC< workspaces, error, filter, + page, + limit, + count, onFilter, - pagination, + onPageChange, onUpdateWorkspace, }) => { const presetFilters = [ @@ -99,7 +102,14 @@ export const WorkspacesPageView: FC< isUsingFilter={filter !== workspaceFilterQuery.me} onUpdateWorkspace={onUpdateWorkspace} /> - {pagination && } + {count && ( + + )} ) } diff --git a/site/src/pages/WorkspacesPage/data.ts b/site/src/pages/WorkspacesPage/data.ts new file mode 100644 index 0000000000000..bbe229203ef1b --- /dev/null +++ b/site/src/pages/WorkspacesPage/data.ts @@ -0,0 +1,113 @@ +import { + QueryKey, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query" +import { getWorkspaces, updateWorkspaceVersion } from "api/api" +import { + Workspace, + WorkspaceBuild, + WorkspacesResponse, +} from "api/typesGenerated" + +const getQueryKey = (page: number, query: string) => { + return ["workspaces", query, page] +} + +type UseWorkspacesDataParams = { + page: number + limit: number + query: string +} + +export const useWorkspacesData = ({ + page, + limit, + query, +}: UseWorkspacesDataParams) => { + const queryKey = getQueryKey(page, query) + 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() + + return useMutation({ + mutationFn: updateWorkspaceVersion, + onMutate: async (workspace) => { + await queryClient.cancelQueries({ queryKey }) + queryClient.setQueryData(queryKey, (oldResponse) => { + if (oldResponse) { + return assignPendingStatus(oldResponse, workspace) + } + }) + }, + onSuccess: (workspaceBuild) => { + queryClient.setQueryData(queryKey, (oldResponse) => { + if (oldResponse) { + return assignLatestBuild(oldResponse, workspaceBuild) + } + }) + }, + }) +} + +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 + }), + } +} From d9dcf0743fa81cd215a0aade30f6244b28d8b7eb Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 24 Jan 2023 14:46:08 +0000 Subject: [PATCH 03/10] Update depds --- site/package.json | 2 +- site/yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/package.json b/site/package.json index 91a4ec6abd155..111802a467a3e 100644 --- a/site/package.json +++ b/site/package.json @@ -37,7 +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", + "@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", diff --git a/site/yarn.lock b/site/yarn.lock index deb7e5505e968..ef8582b8f284f 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -2902,7 +2902,7 @@ 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": +"@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== From ee7da80feca7c528be71ac8960a19a5459dbb2a8 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 24 Jan 2023 14:49:29 +0000 Subject: [PATCH 04/10] Update providers in test --- site/src/app.tsx | 14 ++++++-- site/src/testHelpers/renderHelpers.tsx | 47 +++++++++----------------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/site/src/app.tsx b/site/src/app.tsx index 5e40bc4afac53..4b47895aa4bff 100644 --- a/site/src/app.tsx +++ b/site/src/app.tsx @@ -2,7 +2,7 @@ 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" @@ -19,7 +19,7 @@ const queryClient = new QueryClient({ }, }) -export const App: FC = () => { +export const AppProviders: FC = ({ children }) => { return ( @@ -27,7 +27,7 @@ export const App: FC = () => { - + {children} @@ -36,3 +36,11 @@ export const App: FC = () => { ) } + +export const App: FC = () => { + return ( + + + + ) +} diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index c29f894230e0d..5f8c7a1ce8e73 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -1,17 +1,13 @@ -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, Route, @@ -19,7 +15,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 +23,9 @@ export const WrapperComponent: FC> = ({ children, }) => { return ( - - - - {children} - - - + + {children} + ) } @@ -59,24 +50,18 @@ export function renderWithAuth( }: { route?: string; path?: string; routes?: JSX.Element } = {}, ): RenderWithAuthResult { const renderResult = wrappedRender( - - - - - - - }> - }> - - - - {routes} - - - - - - , + + + + }> + }> + + + + {routes} + + + , ) return { From 8cf28a644e9420d20f2522d82fb946f6e5e5139c Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 24 Jan 2023 17:55:09 +0000 Subject: [PATCH 05/10] Remove unused func --- site/src/pages/WorkspacesPage/data.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/site/src/pages/WorkspacesPage/data.ts b/site/src/pages/WorkspacesPage/data.ts index bbe229203ef1b..001dd678e69cc 100644 --- a/site/src/pages/WorkspacesPage/data.ts +++ b/site/src/pages/WorkspacesPage/data.ts @@ -11,10 +11,6 @@ import { WorkspacesResponse, } from "api/typesGenerated" -const getQueryKey = (page: number, query: string) => { - return ["workspaces", query, page] -} - type UseWorkspacesDataParams = { page: number limit: number @@ -26,7 +22,7 @@ export const useWorkspacesData = ({ limit, query, }: UseWorkspacesDataParams) => { - const queryKey = getQueryKey(page, query) + const queryKey = ["workspaces", query, page] const result = useQuery({ queryKey, queryFn: () => From d8db704b89ea6ef7131627c73ec96747c85093ec Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 24 Jan 2023 17:57:58 +0000 Subject: [PATCH 06/10] Handle update version error --- site/src/i18n/en/workspacesPage.json | 5 +++-- site/src/pages/WorkspacesPage/data.ts | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/site/src/i18n/en/workspacesPage.json b/site/src/i18n/en/workspacesPage.json index 1bcb1b6f77c1b..cd9a698bf3ad1 100644 --- a/site/src/i18n/en/workspacesPage.json +++ b/site/src/i18n/en/workspacesPage.json @@ -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" } diff --git a/site/src/pages/WorkspacesPage/data.ts b/site/src/pages/WorkspacesPage/data.ts index 001dd678e69cc..d46ccf1b8f8d3 100644 --- a/site/src/pages/WorkspacesPage/data.ts +++ b/site/src/pages/WorkspacesPage/data.ts @@ -5,11 +5,14 @@ import { 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 @@ -42,6 +45,7 @@ export const useWorkspacesData = ({ export const useWorkspaceUpdate = (queryKey: QueryKey) => { const queryClient = useQueryClient() + const { t } = useTranslation("workspacesPage") return useMutation({ mutationFn: updateWorkspaceVersion, @@ -60,6 +64,10 @@ export const useWorkspaceUpdate = (queryKey: QueryKey) => { } }) }, + onError: (error) => { + const message = getErrorMessage(error, t("updateVersionError")) + displayError(message) + }, }) } From 5da543837259e8149affbf819cc09c6b6e2599ad Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 24 Jan 2023 18:05:55 +0000 Subject: [PATCH 07/10] Fix empty message --- .../WorkspacesTable/WorkspacesTableBody.tsx | 49 ++++++++++++------- .../WorkspacesPage/WorkspacesPageView.tsx | 2 +- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx b/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx index 1af5a9c67e977..ed93e439c4551 100644 --- a/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx +++ b/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx @@ -3,6 +3,7 @@ import Link from "@material-ui/core/Link" import { makeStyles } from "@material-ui/core/styles" 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" @@ -27,25 +28,32 @@ export const WorkspacesTableBody: FC< } if (workspaces.length === 0) { - return isUsingFilter ? ( - - ) : ( - - - - } - image={ -
- -
- } - /> + return ( + + + + + + + + + + } + image={ +
+ +
+ } + /> +
+
) } @@ -63,6 +71,9 @@ export const WorkspacesTableBody: FC< } const useStyles = makeStyles((theme) => ({ + withImage: { + paddingBottom: 0, + }, emptyImage: { maxWidth: "50%", height: theme.spacing(34), diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index eb5c08afefd27..a7d25e263b6f9 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -102,7 +102,7 @@ export const WorkspacesPageView: FC< isUsingFilter={filter !== workspaceFilterQuery.me} onUpdateWorkspace={onUpdateWorkspace} /> - {count && ( + {count !== undefined && ( Date: Tue, 24 Jan 2023 18:22:19 +0000 Subject: [PATCH 08/10] Add i18n provider for tests --- site/src/testHelpers/renderHelpers.tsx | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index 5f8c7a1ce8e73..6190247f26cf9 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -7,7 +7,9 @@ import { import { AppProviders } from "app" import { DashboardLayout } from "components/Dashboard/DashboardLayout" import { createMemoryHistory } from "history" +import { i18n } from "i18n" import { FC, ReactElement } from "react" +import { I18nextProvider } from "react-i18next" import { MemoryRouter, Route, @@ -50,18 +52,20 @@ export function renderWithAuth( }: { route?: string; path?: string; routes?: JSX.Element } = {}, ): RenderWithAuthResult { const renderResult = wrappedRender( - - - - }> - }> - + + + + + }> + }> + + - - {routes} - - - , + {routes} + + + + , ) return { From 0a6ea091afc604dca3f3de4dd995f5b22ccb901a Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 24 Jan 2023 18:33:55 +0000 Subject: [PATCH 09/10] Remoove global snackbar from providers --- .../AccountPage/AccountPage.test.tsx | 8 +------- .../SSHKeysPage/SSHKeysPage.test.tsx | 15 ++------------- .../SecurityPage/SecurityPage.test.tsx | 8 +------- site/src/pages/UsersPage/UsersPage.test.tsx | 8 +------- 4 files changed, 5 insertions(+), 34 deletions(-) diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx index 9adf8a79db8ad..9a52ac71fcda2 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx @@ -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( - <> - - - , - ) + return renderWithAuth() } const newData = { diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx index 9114f5d6defc0..9d99bcc9849df 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx @@ -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( - <> - - - , - ) + renderWithAuth() // 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( - <> - - - , - ) + renderWithAuth() // Wait to the ssh be rendered on the screen await screen.findByText(MockGitSSHKey.public_key) diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx index b61f44500fb59..89b3dac89e428 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx @@ -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( - <> - - - , - ) + return renderWithAuth() } const newData = { diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index 12051f02ab7c2..4f8c1acb53050 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -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( - <> - - - , - ) + return renderWithAuth() } const suspendUser = async (setupActionSpies: () => void) => { From 4f4638e21084a8ca7623eee1176dd84113bb9712 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 24 Jan 2023 18:48:10 +0000 Subject: [PATCH 10/10] Fix empty states --- .../PaginationWidget/PaginationWidgetBase.tsx | 2 +- .../WorkspacesPageView.stories.tsx | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/site/src/components/PaginationWidget/PaginationWidgetBase.tsx b/site/src/components/PaginationWidget/PaginationWidgetBase.tsx index b145b8ec1a1ff..14fbd36350a66 100644 --- a/site/src/components/PaginationWidget/PaginationWidgetBase.tsx +++ b/site/src/components/PaginationWidget/PaginationWidgetBase.tsx @@ -27,7 +27,7 @@ export const PaginationWidgetBase = ({ const isFirstPage = page === 0 const isLastPage = page === numPages - 1 - if (numPages === 1) { + if (numPages < 2) { return null } diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index fc8060328c679..3c874548a27da 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -1,4 +1,5 @@ import { ComponentMeta, Story } from "@storybook/react" +import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils" import dayjs from "dayjs" import uniqueId from "lodash/uniqueId" import { @@ -57,6 +58,11 @@ const allWorkspaces = [ export default { title: "pages/WorkspacesPageView", component: WorkspacesPageView, + argTypes: { + limit: { + defaultValue: DEFAULT_RECORDS_PER_PAGE, + }, + }, } as ComponentMeta const Template: Story = (args) => ( @@ -76,16 +82,9 @@ OwnerHasNoWorkspaces.args = { count: 0, } -export const NoResults = Template.bind({}) -NoResults.args = { +export const NoSearchResults = Template.bind({}) +NoSearchResults.args = { workspaces: [], filter: "searchtearmwithnoresults", count: 0, } - -export const EmptyPage = Template.bind({}) -EmptyPage.args = { - workspaces: [], - filter: workspaceFilterQuery.me, - count: 0, -}