feat(site): add batch actions to the workspaces page (#9091)

This commit is contained in:
Bruno Quaresma
2023-08-15 12:57:39 -03:00
committed by GitHub
parent c2c9da7db0
commit 4058f049af
17 changed files with 506 additions and 148 deletions
+4 -2
View File
@@ -8055,7 +8055,8 @@ const docTemplate = `{
"single_tailnet",
"template_restart_requirement",
"deployment_health_page",
"template_parameters_insights"
"template_parameters_insights",
"workspaces_batch_actions"
],
"x-enum-varnames": [
"ExperimentMoons",
@@ -8064,7 +8065,8 @@ const docTemplate = `{
"ExperimentSingleTailnet",
"ExperimentTemplateRestartRequirement",
"ExperimentDeploymentHealthPage",
"ExperimentTemplateParametersInsights"
"ExperimentTemplateParametersInsights",
"ExperimentWorkspacesBatchActions"
]
},
"codersdk.Feature": {
+4 -2
View File
@@ -7214,7 +7214,8 @@
"single_tailnet",
"template_restart_requirement",
"deployment_health_page",
"template_parameters_insights"
"template_parameters_insights",
"workspaces_batch_actions"
],
"x-enum-varnames": [
"ExperimentMoons",
@@ -7223,7 +7224,8 @@
"ExperimentSingleTailnet",
"ExperimentTemplateRestartRequirement",
"ExperimentDeploymentHealthPage",
"ExperimentTemplateParametersInsights"
"ExperimentTemplateParametersInsights",
"ExperimentWorkspacesBatchActions"
]
},
"codersdk.Feature": {
+4
View File
@@ -1931,6 +1931,9 @@ const (
// Template parameters insights
ExperimentTemplateParametersInsights Experiment = "template_parameters_insights"
// Workspaces batch actions
ExperimentWorkspacesBatchActions Experiment = "workspaces_batch_actions"
// Add new experiments here!
// ExperimentExample Experiment = "example"
)
@@ -1942,6 +1945,7 @@ const (
var ExperimentsAll = Experiments{
ExperimentDeploymentHealthPage,
ExperimentTemplateParametersInsights,
ExperimentWorkspacesBatchActions,
}
// Experiments is a list of experiments that are enabled for the deployment.
+1
View File
@@ -2711,6 +2711,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `template_restart_requirement` |
| `deployment_health_page` |
| `template_parameters_insights` |
| `workspaces_batch_actions` |
## codersdk.Feature
+2
View File
@@ -1601,6 +1601,7 @@ export type Experiment =
| "template_parameters_insights"
| "template_restart_requirement"
| "workspace_actions"
| "workspaces_batch_actions"
export const Experiments: Experiment[] = [
"deployment_health_page",
"moons",
@@ -1609,6 +1610,7 @@ export const Experiments: Experiment[] = [
"template_parameters_insights",
"template_restart_requirement",
"workspace_actions",
"workspaces_batch_actions",
]
// From codersdk/deployment.go
@@ -1,45 +0,0 @@
import Box from "@mui/material/Box"
import Skeleton from "@mui/material/Skeleton"
type BasePaginationStatusProps = {
label: string
isLoading: boolean
showing?: number
total?: number
}
type LoadedPaginationStatusProps = BasePaginationStatusProps & {
isLoading: false
showing: number
total: number
}
export const PaginationStatus = ({
isLoading,
showing,
total,
label,
}: BasePaginationStatusProps | LoadedPaginationStatusProps) => {
return (
<Box
sx={{
fontSize: 13,
mb: 2,
mt: 1,
color: (theme) => theme.palette.text.secondary,
"& strong": { color: (theme) => theme.palette.text.primary },
}}
>
{!isLoading ? (
<>
Showing <strong>{showing}</strong> of{" "}
<strong>{total?.toLocaleString()}</strong> {label}
</>
) : (
<Box sx={{ height: 24, display: "flex", alignItems: "center" }}>
<Skeleton variant="text" width={160} height={16} />
</Box>
)}
</Box>
)
}
@@ -0,0 +1,48 @@
import { styled } from "@mui/material/styles"
import Box from "@mui/material/Box"
import Skeleton from "@mui/material/Skeleton"
export const TableToolbar = styled(Box)(({ theme }) => ({
fontSize: 13,
marginBottom: theme.spacing(1),
marginTop: theme.spacing(0),
height: 36, // The size of a small button
color: theme.palette.text.secondary,
"& strong": { color: theme.palette.text.primary },
display: "flex",
alignItems: "center",
}))
type BasePaginationStatusProps = {
label: string
isLoading: boolean
showing?: number
total?: number
}
type LoadedPaginationStatusProps = BasePaginationStatusProps & {
isLoading: false
showing: number
total: number
}
export const PaginationStatus = ({
isLoading,
showing,
total,
label,
}: BasePaginationStatusProps | LoadedPaginationStatusProps) => {
if (isLoading) {
return (
<Box sx={{ height: 24, display: "flex", alignItems: "center" }}>
<Skeleton variant="text" width={160} height={16} />
</Box>
)
}
return (
<Box>
Showing <strong>{showing}</strong> of{" "}
<strong>{total?.toLocaleString()}</strong> {label}
</Box>
)
}
+12 -7
View File
@@ -21,7 +21,10 @@ import { ComponentProps, FC } from "react"
import { useTranslation } from "react-i18next"
import { AuditPaywall } from "./AuditPaywall"
import { AuditFilter } from "./AuditFilter"
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
import {
PaginationStatus,
TableToolbar,
} from "components/TableToolbar/TableToolbar"
import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase"
export const Language = {
@@ -73,12 +76,14 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
<Cond condition={isAuditLogVisible}>
<AuditFilter {...filterProps} />
<PaginationStatus
isLoading={Boolean(isLoading)}
showing={auditLogs?.length ?? 0}
total={count ?? 0}
label="audit logs"
/>
<TableToolbar>
<PaginationStatus
isLoading={Boolean(isLoading)}
showing={auditLogs?.length ?? 0}
total={count ?? 0}
label="audit logs"
/>
</TableToolbar>
<TableContainer>
<Table>
+12 -7
View File
@@ -33,7 +33,10 @@ import { pageTitle } from "utils/page"
import { groupMachine } from "xServices/groups/groupXService"
import { Maybe } from "components/Conditionals/Maybe"
import { makeStyles } from "@mui/styles"
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
import {
PaginationStatus,
TableToolbar,
} from "components/TableToolbar/TableToolbar"
import { UserAvatar } from "components/UserAvatar/UserAvatar"
const AddGroupMember: React.FC<{
@@ -155,12 +158,14 @@ export const GroupPage: React.FC = () => {
}}
/>
</Maybe>
<PaginationStatus
isLoading={Boolean(isLoading)}
showing={group?.members.length ?? 0}
total={group?.members.length ?? 0}
label="members"
/>
<TableToolbar>
<PaginationStatus
isLoading={Boolean(isLoading)}
showing={group?.members.length ?? 0}
total={group?.members.length ?? 0}
label="members"
/>
</TableToolbar>
<TableContainer>
<Table>
+12 -7
View File
@@ -4,7 +4,10 @@ import { PaginationMachineRef } from "xServices/pagination/paginationXService"
import * as TypesGen from "../../api/typesGenerated"
import { UsersTable } from "../../components/UsersTable/UsersTable"
import { UsersFilter } from "./UsersFilter"
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
import {
PaginationStatus,
TableToolbar,
} from "components/TableToolbar/TableToolbar"
export const Language = {
activeUsersFilterName: "Active users",
@@ -60,12 +63,14 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
<>
<UsersFilter {...filterProps} />
<PaginationStatus
isLoading={Boolean(isLoading)}
showing={users?.length ?? 0}
total={count ?? 0}
label="users"
/>
<TableToolbar>
<PaginationStatus
isLoading={Boolean(isLoading)}
showing={users?.length ?? 0}
total={count ?? 0}
label="users"
/>
</TableToolbar>
<UsersTable
users={users}
@@ -1,11 +1,17 @@
import { screen } from "@testing-library/react"
import { screen, waitFor, within } from "@testing-library/react"
import { rest } from "msw"
import * as CreateDayString from "utils/createDayString"
import { MockWorkspace, MockWorkspacesResponse } from "testHelpers/entities"
import { renderWithAuth } from "testHelpers/renderHelpers"
import {
renderWithAuth,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers"
import { server } from "testHelpers/server"
import WorkspacesPage from "./WorkspacesPage"
import { i18n } from "i18n"
import userEvent from "@testing-library/user-event"
import * as API from "api/api"
import { Workspace } from "api/typesGenerated"
const { t } = i18n
@@ -40,4 +46,37 @@ describe("WorkspacesPage", () => {
)
expect(templateDisplayNames).toHaveLength(MockWorkspacesResponse.count)
})
it("deletes only the selected workspaces", async () => {
const workspaces = [
{ ...MockWorkspace, id: "1" },
{ ...MockWorkspace, id: "2" },
{ ...MockWorkspace, id: "3" },
]
jest
.spyOn(API, "getWorkspaces")
.mockResolvedValue({ workspaces, count: workspaces.length })
const deleteWorkspace = jest.spyOn(API, "deleteWorkspace")
const user = userEvent.setup()
renderWithAuth(<WorkspacesPage />)
await waitForLoaderToBeRemoved()
await user.click(getWorkspaceCheckbox(workspaces[0]))
await user.click(getWorkspaceCheckbox(workspaces[1]))
await user.click(screen.getByRole("button", { name: /delete all/i }))
await user.type(screen.getByLabelText(/type delete to confirm/i), "DELETE")
await user.click(screen.getByTestId("confirm-button"))
await waitFor(() => {
expect(deleteWorkspace).toHaveBeenCalledTimes(2)
})
expect(deleteWorkspace).toHaveBeenCalledWith(workspaces[0].id)
expect(deleteWorkspace).toHaveBeenCalledWith(workspaces[1].id)
})
})
const getWorkspaceCheckbox = (workspace: Workspace) => {
return within(screen.getByTestId(`checkbox-${workspace.id}`)).getByRole(
"checkbox",
)
}
@@ -1,6 +1,9 @@
import { usePagination } from "hooks/usePagination"
import { Workspace } from "api/typesGenerated"
import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider"
import {
useDashboard,
useIsWorkspaceActionsEnabled,
} from "components/Dashboard/DashboardProvider"
import { FC, useEffect, useState } from "react"
import { Helmet } from "react-helmet-async"
import { pageTitle } from "utils/page"
@@ -11,7 +14,13 @@ import { useTemplateFilterMenu, useStatusFilterMenu } from "./filter/menus"
import { useSearchParams } from "react-router-dom"
import { useFilter } from "components/Filter/filter"
import { useUserFilterMenu } from "components/Filter/UserFilter"
import { getWorkspaces } from "api/api"
import { deleteWorkspace, getWorkspaces } from "api/api"
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
import Box from "@mui/material/Box"
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
import TextField from "@mui/material/TextField"
import { displayError } from "components/GlobalSnackbar/utils"
import { getErrorMessage } from "api/errors"
const WorkspacesPage: FC = () => {
const [lockedWorkspaces, setLockedWorkspaces] = useState<Workspace[]>([])
@@ -21,7 +30,7 @@ const WorkspacesPage: FC = () => {
const searchParamsResult = useSearchParams()
const pagination = usePagination({ searchParamsResult })
const filterProps = useWorkspacesFilter({ searchParamsResult, pagination })
const { data, error, queryKey } = useWorkspacesData({
const { data, error, queryKey, refetch } = useWorkspacesData({
...pagination,
query: filterProps.filter.query,
})
@@ -55,8 +64,20 @@ const WorkspacesPage: FC = () => {
setLockedWorkspaces([])
}
}, [experimentEnabled, data, filterProps.filter.query])
const updateWorkspace = useWorkspaceUpdate(queryKey)
const [checkedWorkspaces, setCheckedWorkspaces] = useState<Workspace[]>([])
const [isDeletingAll, setIsDeletingAll] = useState(false)
const [urlSearchParams] = searchParamsResult
const dashboard = useDashboard()
const isWorkspaceBatchActionsEnabled =
dashboard.experiments.includes("workspaces_batch_actions") ||
process.env.NODE_ENV === "development"
// We want to uncheck the selected workspaces always when the url changes
// because of filtering or pagination
useEffect(() => {
setCheckedWorkspaces([])
}, [urlSearchParams])
return (
<>
@@ -65,6 +86,9 @@ const WorkspacesPage: FC = () => {
</Helmet>
<WorkspacesPageView
isWorkspaceBatchActionsEnabled={isWorkspaceBatchActionsEnabled}
checkedWorkspaces={checkedWorkspaces}
onCheckChange={setCheckedWorkspaces}
workspaces={data?.workspaces}
lockedWorkspaces={lockedWorkspaces}
error={error}
@@ -76,6 +100,21 @@ const WorkspacesPage: FC = () => {
onUpdateWorkspace={(workspace) => {
updateWorkspace.mutate(workspace)
}}
onDeleteAll={() => {
setIsDeletingAll(true)
}}
/>
<BatchDeleteConfirmation
checkedWorkspaces={checkedWorkspaces}
open={isDeletingAll}
onClose={() => {
setIsDeletingAll(false)
}}
onDelete={async () => {
await refetch()
setCheckedWorkspaces([])
}}
/>
</>
)
@@ -129,3 +168,109 @@ const useWorkspacesFilter = ({
},
}
}
const BatchDeleteConfirmation = ({
checkedWorkspaces,
open,
onClose,
onDelete,
}: {
checkedWorkspaces: Workspace[]
open: boolean
onClose: () => void
onDelete: () => void
}) => {
const [confirmValue, setConfirmValue] = useState("")
const [confirmError, setConfirmError] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const close = () => {
if (isDeleting) {
return
}
onClose()
setConfirmValue("")
setConfirmError(false)
setIsDeleting(false)
}
const confirmDeletion = async () => {
setConfirmError(false)
if (confirmValue.toLowerCase() !== "delete") {
setConfirmError(true)
return
}
try {
setIsDeleting(true)
await Promise.all(checkedWorkspaces.map((w) => deleteWorkspace(w.id)))
} catch (e) {
displayError(
"Error on deleting workspaces",
getErrorMessage(e, "An error occurred while deleting the workspaces"),
)
} finally {
close()
onDelete()
}
}
return (
<ConfirmDialog
type="delete"
open={open}
confirmLoading={isDeleting}
onConfirm={confirmDeletion}
onClose={() => {
onClose()
setConfirmValue("")
setConfirmError(false)
}}
title={`Delete ${checkedWorkspaces?.length} ${
checkedWorkspaces.length === 1 ? "workspace" : "workspaces"
}`}
description={
<form
onSubmit={async (e) => {
e.preventDefault()
await confirmDeletion()
}}
>
<Box>
Deleting these workspaces is irreversible! Are you sure you want to
proceed? Type{" "}
<Box
component="code"
sx={{
fontFamily: MONOSPACE_FONT_FAMILY,
color: (theme) => theme.palette.text.primary,
fontWeight: 600,
}}
>
`DELETE`
</Box>{" "}
to confirm.
</Box>
<TextField
value={confirmValue}
required
autoFocus
fullWidth
inputProps={{
"aria-label": "Type DELETE to confirm",
}}
placeholder="Type DELETE to confirm"
sx={{ mt: 2 }}
onChange={(e) => {
setConfirmValue(e.currentTarget.value)
}}
error={confirmError}
helperText={confirmError && "Please type DELETE to confirm"}
/>
</form>
}
/>
)
}
@@ -96,6 +96,7 @@ const meta: Meta<typeof WorkspacesPageView> = {
args: {
limit: DEFAULT_RECORDS_PER_PAGE,
filterProps: defaultFilterProps,
checkedWorkspaces: [],
},
decorators: [
(Story) => (
@@ -18,7 +18,13 @@ import { LockedWorkspaceBanner, Count } from "components/WorkspaceDeletion"
import { ErrorAlert } from "components/Alert/ErrorAlert"
import { WorkspacesFilter } from "./filter/filter"
import { hasError, isApiValidationError } from "api/errors"
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
import {
PaginationStatus,
TableToolbar,
} from "components/TableToolbar/TableToolbar"
import Box from "@mui/material/Box"
import Button from "@mui/material/Button"
import DeleteOutlined from "@mui/icons-material/DeleteOutlined"
export const Language = {
pageTitle: "Workspaces",
@@ -33,12 +39,16 @@ export interface WorkspacesPageViewProps {
error: unknown
workspaces?: Workspace[]
lockedWorkspaces?: Workspace[]
checkedWorkspaces: Workspace[]
count?: number
filterProps: ComponentProps<typeof WorkspacesFilter>
page: number
limit: number
isWorkspaceBatchActionsEnabled?: boolean
onPageChange: (page: number) => void
onUpdateWorkspace: (workspace: Workspace) => void
onCheckChange: (checkedWorkspaces: Workspace[]) => void
onDeleteAll: () => void
}
export const WorkspacesPageView: FC<
@@ -53,6 +63,10 @@ export const WorkspacesPageView: FC<
onPageChange,
onUpdateWorkspace,
page,
checkedWorkspaces,
isWorkspaceBatchActionsEnabled,
onCheckChange,
onDeleteAll,
}) => {
const { saveLocal } = useLocalStorage()
@@ -102,17 +116,42 @@ export const WorkspacesPageView: FC<
<WorkspacesFilter error={error} {...filterProps} />
</Stack>
<PaginationStatus
isLoading={!workspaces && !error}
showing={workspaces?.length ?? 0}
total={count ?? 0}
label="workspaces"
/>
<TableToolbar>
{checkedWorkspaces.length > 0 ? (
<>
<Box>
Selected <strong>{checkedWorkspaces.length}</strong> of{" "}
<strong>{workspaces?.length}</strong>{" "}
{workspaces?.length === 1 ? "workspace" : "workspaces"}
</Box>
<Box sx={{ marginLeft: "auto" }}>
<Button
size="small"
startIcon={<DeleteOutlined />}
onClick={onDeleteAll}
>
Delete all
</Button>
</Box>
</>
) : (
<PaginationStatus
isLoading={!workspaces && !error}
showing={workspaces?.length ?? 0}
total={count ?? 0}
label="workspaces"
/>
)}
</TableToolbar>
<WorkspacesTable
workspaces={workspaces}
isUsingFilter={filterProps.filter.used}
onUpdateWorkspace={onUpdateWorkspace}
checkedWorkspaces={checkedWorkspaces}
onCheckChange={onCheckChange}
isWorkspaceBatchActionsEnabled={isWorkspaceBatchActionsEnabled}
/>
{count !== undefined && (
<PaginationWidgetBase
+160 -65
View File
@@ -31,18 +31,25 @@ import { LastUsed } from "components/LastUsed/LastUsed"
import { WorkspaceOutdatedTooltip } from "components/Tooltips"
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
import { getDisplayWorkspaceTemplateName } from "utils/workspace"
import Checkbox from "@mui/material/Checkbox"
export interface WorkspacesTableProps {
workspaces?: Workspace[]
isUsingFilter: boolean
onUpdateWorkspace: (workspace: Workspace) => void
checkedWorkspaces: Workspace[]
error?: unknown
isUsingFilter: boolean
isWorkspaceBatchActionsEnabled?: boolean
onUpdateWorkspace: (workspace: Workspace) => void
onCheckChange: (checkedWorkspaces: Workspace[]) => void
}
export const WorkspacesTable: FC<WorkspacesTableProps> = ({
workspaces,
checkedWorkspaces,
isUsingFilter,
isWorkspaceBatchActionsEnabled,
onUpdateWorkspace,
onCheckChange,
}) => {
const { t } = useTranslation("workspacesPage")
const styles = useStyles()
@@ -52,7 +59,37 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
<Table>
<TableHead>
<TableRow>
<TableCell width="40%">Name</TableCell>
{isWorkspaceBatchActionsEnabled ? (
<TableCell
width="40%"
sx={{
paddingLeft: (theme) => `${theme.spacing(1.5)} !important`,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Checkbox
disabled={!workspaces || workspaces.length === 0}
checked={checkedWorkspaces.length === workspaces?.length}
size="small"
onChange={(_, checked) => {
if (!workspaces) {
return
}
if (!checked) {
onCheckChange([])
} else {
onCheckChange(workspaces)
}
}}
/>
Name
</Box>
</TableCell>
) : (
<TableCell width="40%">Name</TableCell>
)}
<TableCell width="25%">Template</TableCell>
<TableCell width="20%">Last used</TableCell>
<TableCell width="15%">Status</TableCell>
@@ -92,71 +129,117 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
</ChooseOne>
)}
{workspaces &&
workspaces.map((workspace) => (
<WorkspacesRow workspace={workspace} key={workspace.id}>
<TableCell>
<AvatarData
title={
<Stack direction="row" spacing={0} alignItems="center">
{workspace.name}
{workspace.outdated && (
<WorkspaceOutdatedTooltip
templateName={workspace.template_name}
templateId={workspace.template_id}
onUpdateVersion={() => {
onUpdateWorkspace(workspace)
}}
/>
)}
</Stack>
}
subtitle={workspace.owner_name}
avatar={
<Avatar
src={workspace.template_icon}
variant={workspace.template_icon ? "square" : undefined}
fitImage={Boolean(workspace.template_icon)}
>
{workspace.name}
</Avatar>
}
/>
</TableCell>
<TableCell>
{getDisplayWorkspaceTemplateName(workspace)}
</TableCell>
<TableCell>
<LastUsed lastUsedAt={workspace.last_used_at} />
</TableCell>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<WorkspaceStatusBadge workspace={workspace} />
{workspace.latest_build.status === "running" &&
!workspace.health.healthy && <UnhealthyTooltip />}
</Box>
</TableCell>
<TableCell>
<Box
workspaces.map((workspace) => {
const checked = checkedWorkspaces.some(
(w) => w.id === workspace.id,
)
return (
<WorkspacesRow
workspace={workspace}
key={workspace.id}
checked={checked}
>
<TableCell
sx={{
display: "flex",
paddingLeft: (theme) => theme.spacing(2),
paddingLeft: (theme) =>
isWorkspaceBatchActionsEnabled
? `${theme.spacing(1.5)} !important`
: undefined,
}}
>
<KeyboardArrowRight
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{isWorkspaceBatchActionsEnabled && (
<Checkbox
data-testid={`checkbox-${workspace.id}`}
size="small"
disabled={cantBeChecked(workspace)}
checked={checked}
onClick={(e) => {
e.stopPropagation()
}}
onChange={(e) => {
if (e.currentTarget.checked) {
onCheckChange([...checkedWorkspaces, workspace])
} else {
onCheckChange(
checkedWorkspaces.filter(
(w) => w.id !== workspace.id,
),
)
}
}}
/>
)}
<AvatarData
title={
<Stack
direction="row"
spacing={0}
alignItems="center"
>
{workspace.name}
{workspace.outdated && (
<WorkspaceOutdatedTooltip
templateName={workspace.template_name}
templateId={workspace.template_id}
onUpdateVersion={() => {
onUpdateWorkspace(workspace)
}}
/>
)}
</Stack>
}
subtitle={workspace.owner_name}
avatar={
<Avatar
src={workspace.template_icon}
variant={
workspace.template_icon ? "square" : undefined
}
fitImage={Boolean(workspace.template_icon)}
>
{workspace.name}
</Avatar>
}
/>
</Box>
</TableCell>
<TableCell>
{getDisplayWorkspaceTemplateName(workspace)}
</TableCell>
<TableCell>
<LastUsed lastUsedAt={workspace.last_used_at} />
</TableCell>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<WorkspaceStatusBadge workspace={workspace} />
{workspace.latest_build.status === "running" &&
!workspace.health.healthy && <UnhealthyTooltip />}
</Box>
</TableCell>
<TableCell>
<Box
sx={{
color: (theme) => theme.palette.text.secondary,
width: 20,
height: 20,
display: "flex",
paddingLeft: (theme) => theme.spacing(2),
}}
/>
</Box>
</TableCell>
</WorkspacesRow>
))}
>
<KeyboardArrowRight
sx={{
color: (theme) => theme.palette.text.secondary,
width: 20,
height: 20,
}}
/>
</Box>
</TableCell>
</WorkspacesRow>
)
})}
</TableBody>
</Table>
</TableContainer>
@@ -166,7 +249,8 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
const WorkspacesRow: FC<{
workspace: Workspace
children: ReactNode
}> = ({ workspace, children }) => {
checked: boolean
}> = ({ workspace, children, checked }) => {
const navigate = useNavigate()
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`
const clickable = useClickableTableRow(() => {
@@ -174,7 +258,14 @@ const WorkspacesRow: FC<{
})
return (
<TableRow data-testid={`workspace-${workspace.id}`} {...clickable}>
<TableRow
data-testid={`workspace-${workspace.id}`}
{...clickable}
sx={{
backgroundColor: (theme) =>
checked ? theme.palette.action.hover : undefined,
}}
>
{children}
</TableRow>
)
@@ -198,6 +289,10 @@ export const UnhealthyTooltip = () => {
)
}
const cantBeChecked = (workspace: Workspace) => {
return ["deleting", "pending"].includes(workspace.latest_build.status)
}
const useUnhealthyTooltipStyles = makeStyles(() => ({
unhealthyIcon: {
color: colors.yellow[5],
+1
View File
@@ -1507,6 +1507,7 @@ export const MockEntitlementsWithScheduling: TypesGen.Entitlements = {
export const MockExperiments: TypesGen.Experiment[] = [
"workspace_actions",
"moons",
"workspaces_batch_actions",
]
export const MockAuditLog: TypesGen.AuditLog = {
+9
View File
@@ -384,6 +384,15 @@ dark = createTheme(dark, {
disableRipple: true,
},
},
MuiCheckbox: {
styleOverrides: {
root: {
"&.Mui-disabled": {
color: colors.gray[11],
},
},
},
},
MuiSwitch: {
defaultProps: {
color: "primary",