feat(site): make workspace batch deletion GA (#9313)

This commit is contained in:
Bruno Quaresma
2023-08-30 10:08:42 -03:00
committed by GitHub
parent 90acf998bf
commit 2399063a56
15 changed files with 192 additions and 92 deletions

6
coderd/apidoc/docs.go generated
View File

@ -8125,8 +8125,7 @@ const docTemplate = `{
"tailnet_pg_coordinator", "tailnet_pg_coordinator",
"single_tailnet", "single_tailnet",
"template_autostop_requirement", "template_autostop_requirement",
"deployment_health_page", "deployment_health_page"
"workspaces_batch_actions"
], ],
"x-enum-varnames": [ "x-enum-varnames": [
"ExperimentMoons", "ExperimentMoons",
@ -8134,8 +8133,7 @@ const docTemplate = `{
"ExperimentTailnetPGCoordinator", "ExperimentTailnetPGCoordinator",
"ExperimentSingleTailnet", "ExperimentSingleTailnet",
"ExperimentTemplateAutostopRequirement", "ExperimentTemplateAutostopRequirement",
"ExperimentDeploymentHealthPage", "ExperimentDeploymentHealthPage"
"ExperimentWorkspacesBatchActions"
] ]
}, },
"codersdk.Feature": { "codersdk.Feature": {

View File

@ -7276,8 +7276,7 @@
"tailnet_pg_coordinator", "tailnet_pg_coordinator",
"single_tailnet", "single_tailnet",
"template_autostop_requirement", "template_autostop_requirement",
"deployment_health_page", "deployment_health_page"
"workspaces_batch_actions"
], ],
"x-enum-varnames": [ "x-enum-varnames": [
"ExperimentMoons", "ExperimentMoons",
@ -7285,8 +7284,7 @@
"ExperimentTailnetPGCoordinator", "ExperimentTailnetPGCoordinator",
"ExperimentSingleTailnet", "ExperimentSingleTailnet",
"ExperimentTemplateAutostopRequirement", "ExperimentTemplateAutostopRequirement",
"ExperimentDeploymentHealthPage", "ExperimentDeploymentHealthPage"
"ExperimentWorkspacesBatchActions"
] ]
}, },
"codersdk.Feature": { "codersdk.Feature": {

View File

@ -48,6 +48,7 @@ const (
FeatureAdvancedTemplateScheduling FeatureName = "advanced_template_scheduling" FeatureAdvancedTemplateScheduling FeatureName = "advanced_template_scheduling"
FeatureTemplateAutostopRequirement FeatureName = "template_autostop_requirement" FeatureTemplateAutostopRequirement FeatureName = "template_autostop_requirement"
FeatureWorkspaceProxy FeatureName = "workspace_proxy" FeatureWorkspaceProxy FeatureName = "workspace_proxy"
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions"
) )
// FeatureNames must be kept in-sync with the Feature enum above. // FeatureNames must be kept in-sync with the Feature enum above.
@ -64,6 +65,7 @@ var FeatureNames = []FeatureName{
FeatureAdvancedTemplateScheduling, FeatureAdvancedTemplateScheduling,
FeatureWorkspaceProxy, FeatureWorkspaceProxy,
FeatureUserRoleManagement, FeatureUserRoleManagement,
FeatureWorkspaceBatchActions,
} }
// Humanize returns the feature name in a human-readable format. // Humanize returns the feature name in a human-readable format.
@ -1958,9 +1960,6 @@ const (
// Deployment health page // Deployment health page
ExperimentDeploymentHealthPage Experiment = "deployment_health_page" ExperimentDeploymentHealthPage Experiment = "deployment_health_page"
// Workspaces batch actions
ExperimentWorkspacesBatchActions Experiment = "workspaces_batch_actions"
// Add new experiments here! // Add new experiments here!
// ExperimentExample Experiment = "example" // ExperimentExample Experiment = "example"
) )
@ -1971,7 +1970,6 @@ const (
// not be included here and will be essentially hidden. // not be included here and will be essentially hidden.
var ExperimentsAll = Experiments{ var ExperimentsAll = Experiments{
ExperimentDeploymentHealthPage, ExperimentDeploymentHealthPage,
ExperimentWorkspacesBatchActions,
} }
// Experiments is a list of experiments that are enabled for the deployment. // Experiments is a list of experiments that are enabled for the deployment.

1
docs/api/schemas.md generated
View File

@ -2721,7 +2721,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `single_tailnet` | | `single_tailnet` |
| `template_autostop_requirement` | | `template_autostop_requirement` |
| `deployment_health_page` | | `deployment_health_page` |
| `workspaces_batch_actions` |
## codersdk.Feature ## codersdk.Feature

View File

@ -1606,7 +1606,6 @@ export type Experiment =
| "tailnet_pg_coordinator" | "tailnet_pg_coordinator"
| "template_autostop_requirement" | "template_autostop_requirement"
| "workspace_actions" | "workspace_actions"
| "workspaces_batch_actions"
export const Experiments: Experiment[] = [ export const Experiments: Experiment[] = [
"deployment_health_page", "deployment_health_page",
"moons", "moons",
@ -1614,7 +1613,6 @@ export const Experiments: Experiment[] = [
"tailnet_pg_coordinator", "tailnet_pg_coordinator",
"template_autostop_requirement", "template_autostop_requirement",
"workspace_actions", "workspace_actions",
"workspaces_batch_actions",
] ]
// From codersdk/deployment.go // From codersdk/deployment.go
@ -1631,6 +1629,7 @@ export type FeatureName =
| "template_rbac" | "template_rbac"
| "user_limit" | "user_limit"
| "user_role_management" | "user_role_management"
| "workspace_batch_actions"
| "workspace_proxy" | "workspace_proxy"
export const FeatureNames: FeatureName[] = [ export const FeatureNames: FeatureName[] = [
"advanced_template_scheduling", "advanced_template_scheduling",
@ -1645,6 +1644,7 @@ export const FeatureNames: FeatureName[] = [
"template_rbac", "template_rbac",
"user_limit", "user_limit",
"user_role_management", "user_role_management",
"workspace_batch_actions",
"workspace_proxy", "workspace_proxy",
] ]

View File

@ -1,9 +1,7 @@
import { makeStyles } from "@mui/styles" import { makeStyles } from "@mui/styles"
import TableCell from "@mui/material/TableCell" import TableCell from "@mui/material/TableCell"
import TableRow from "@mui/material/TableRow" import TableRow, { TableRowProps } from "@mui/material/TableRow"
import Skeleton from "@mui/material/Skeleton" import { FC, ReactNode, cloneElement, isValidElement } from "react"
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"
import { FC } from "react"
import { Loader } from "../Loader/Loader" import { Loader } from "../Loader/Loader"
export const TableLoader: FC = () => { export const TableLoader: FC = () => {
@ -25,35 +23,27 @@ const useStyles = makeStyles((theme) => ({
}, },
})) }))
export const TableLoaderSkeleton: FC<{ export const TableLoaderSkeleton = ({
columns: number rows = 4,
children,
}: {
rows?: number rows?: number
useAvatarData?: boolean children: ReactNode
}> = ({ columns, rows = 4, useAvatarData = false }) => { }) => {
const placeholderColumns = Array(columns).fill(undefined) if (!isValidElement(children)) {
const placeholderRows = Array(rows).fill(undefined) throw new Error(
"TableLoaderSkeleton children must be a valid React element",
)
}
return ( return (
<> <>
{placeholderRows.map((_, rowIndex) => ( {Array.from({ length: rows }, (_, i) =>
<TableRow key={rowIndex} role="progressbar" data-testid="loader"> cloneElement(children, { key: i }),
{placeholderColumns.map((_, columnIndex) => { )}
if (useAvatarData && columnIndex === 0) {
return (
<TableCell key={columnIndex}>
<AvatarDataSkeleton />
</TableCell>
)
}
return (
<TableCell key={columnIndex}>
<Skeleton variant="text" width="25%" />
</TableCell>
)
})}
</TableRow>
))}
</> </>
) )
} }
export const TableRowSkeleton = (props: TableRowProps) => {
return <TableRow role="progressbar" data-testid="loader" {...props} />
}

View File

@ -10,7 +10,10 @@ import * as TypesGen from "../../api/typesGenerated"
import { combineClasses } from "../../utils/combineClasses" import { combineClasses } from "../../utils/combineClasses"
import { AvatarData } from "../AvatarData/AvatarData" import { AvatarData } from "../AvatarData/AvatarData"
import { EmptyState } from "../EmptyState/EmptyState" import { EmptyState } from "../EmptyState/EmptyState"
import { TableLoaderSkeleton } from "../TableLoader/TableLoader" import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "../TableLoader/TableLoader"
import { TableRowMenu } from "../TableRowMenu/TableRowMenu" import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
import { EditRolesButton } from "components/EditRolesButton/EditRolesButton" import { EditRolesButton } from "components/EditRolesButton/EditRolesButton"
import { Stack } from "components/Stack/Stack" import { Stack } from "components/Stack/Stack"
@ -23,6 +26,8 @@ import GitHub from "@mui/icons-material/GitHub"
import PasswordOutlined from "@mui/icons-material/PasswordOutlined" import PasswordOutlined from "@mui/icons-material/PasswordOutlined"
import relativeTime from "dayjs/plugin/relativeTime" import relativeTime from "dayjs/plugin/relativeTime"
import ShieldOutlined from "@mui/icons-material/ShieldOutlined" import ShieldOutlined from "@mui/icons-material/ShieldOutlined"
import Skeleton from "@mui/material/Skeleton"
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
@ -91,7 +96,29 @@ export const UsersTableBody: FC<
return ( return (
<ChooseOne> <ChooseOne>
<Cond condition={Boolean(isLoading)}> <Cond condition={Boolean(isLoading)}>
<TableLoaderSkeleton columns={canEditUsers ? 5 : 4} useAvatarData /> <TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<AvatarDataSkeleton />
</Box>
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
{canEditUsers && (
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
)}
</TableRowSkeleton>
</TableLoaderSkeleton>
</Cond> </Cond>
<Cond condition={!users || users.length === 0}> <Cond condition={!users || users.length === 0}>
<ChooseOne> <ChooseOne>

View File

@ -15,7 +15,10 @@ import { AvatarData } from "components/AvatarData/AvatarData"
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
import { EmptyState } from "components/EmptyState/EmptyState" import { EmptyState } from "components/EmptyState/EmptyState"
import { Stack } from "components/Stack/Stack" import { Stack } from "components/Stack/Stack"
import { TableLoaderSkeleton } from "components/TableLoader/TableLoader" import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader"
import { UserAvatar } from "components/UserAvatar/UserAvatar" import { UserAvatar } from "components/UserAvatar/UserAvatar"
import { FC } from "react" import { FC } from "react"
import { Link as RouterLink, useNavigate } from "react-router-dom" import { Link as RouterLink, useNavigate } from "react-router-dom"
@ -23,6 +26,9 @@ import { Paywall } from "components/Paywall/Paywall"
import { Group } from "api/typesGenerated" import { Group } from "api/typesGenerated"
import { GroupAvatar } from "components/GroupAvatar/GroupAvatar" import { GroupAvatar } from "components/GroupAvatar/GroupAvatar"
import { docs } from "utils/docs" import { docs } from "utils/docs"
import Skeleton from "@mui/material/Skeleton"
import { Box } from "@mui/system"
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"
export type GroupsPageViewProps = { export type GroupsPageViewProps = {
groups: Group[] | undefined groups: Group[] | undefined
@ -83,7 +89,7 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
<TableBody> <TableBody>
<ChooseOne> <ChooseOne>
<Cond condition={isLoading}> <Cond condition={isLoading}>
<TableLoaderSkeleton columns={3} useAvatarData /> <TableLoader />
</Cond> </Cond>
<Cond condition={isEmpty}> <Cond condition={isEmpty}>
@ -184,6 +190,26 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
) )
} }
const TableLoader = () => {
return (
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<AvatarDataSkeleton />
</Box>
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
</TableRowSkeleton>
</TableLoaderSkeleton>
)
}
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
clickableTableRow: { clickableTableRow: {
cursor: "pointer", cursor: "pointer",

View File

@ -24,7 +24,10 @@ import {
PageHeaderTitle, PageHeaderTitle,
} from "../../components/PageHeader/PageHeader" } from "../../components/PageHeader/PageHeader"
import { Stack } from "../../components/Stack/Stack" import { Stack } from "../../components/Stack/Stack"
import { TableLoaderSkeleton } from "../../components/TableLoader/TableLoader" import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "../../components/TableLoader/TableLoader"
import { import {
HelpTooltip, HelpTooltip,
HelpTooltipLink, HelpTooltipLink,
@ -42,6 +45,9 @@ import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined"
import { Avatar } from "components/Avatar/Avatar" import { Avatar } from "components/Avatar/Avatar"
import { ErrorAlert } from "components/Alert/ErrorAlert" import { ErrorAlert } from "components/Alert/ErrorAlert"
import { docs } from "utils/docs" import { docs } from "utils/docs"
import Skeleton from "@mui/material/Skeleton"
import { Box } from "@mui/system"
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"
export const Language = { export const Language = {
developerCount: (activeCount: number): string => { developerCount: (activeCount: number): string => {
@ -196,7 +202,7 @@ export const TemplatesPageView: FC<
</TableHead> </TableHead>
<TableBody> <TableBody>
<Maybe condition={isLoading}> <Maybe condition={isLoading}>
<TableLoaderSkeleton columns={5} useAvatarData /> <TableLoader />
</Maybe> </Maybe>
<ChooseOne> <ChooseOne>
@ -222,6 +228,32 @@ export const TemplatesPageView: FC<
) )
} }
const TableLoader = () => {
return (
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<AvatarDataSkeleton />
</Box>
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
</TableRowSkeleton>
</TableLoaderSkeleton>
)
}
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
templateIconWrapper: { templateIconWrapper: {
// Same size then the avatar component // Same size then the avatar component

View File

@ -63,7 +63,7 @@ describe("WorkspacesPage", () => {
await user.click(getWorkspaceCheckbox(workspaces[0])) await user.click(getWorkspaceCheckbox(workspaces[0]))
await user.click(getWorkspaceCheckbox(workspaces[1])) await user.click(getWorkspaceCheckbox(workspaces[1]))
await user.click(screen.getByRole("button", { name: /delete all/i })) await user.click(screen.getByRole("button", { name: /delete selected/i }))
await user.type(screen.getByLabelText(/type delete to confirm/i), "DELETE") await user.type(screen.getByLabelText(/type delete to confirm/i), "DELETE")
await user.click(screen.getByTestId("confirm-button")) await user.click(screen.getByTestId("confirm-button"))

View File

@ -68,10 +68,9 @@ const WorkspacesPage: FC = () => {
const [checkedWorkspaces, setCheckedWorkspaces] = useState<Workspace[]>([]) const [checkedWorkspaces, setCheckedWorkspaces] = useState<Workspace[]>([])
const [isDeletingAll, setIsDeletingAll] = useState(false) const [isDeletingAll, setIsDeletingAll] = useState(false)
const [urlSearchParams] = searchParamsResult const [urlSearchParams] = searchParamsResult
const dashboard = useDashboard() const { entitlements } = useDashboard()
const isWorkspaceBatchActionsEnabled = const canCheckWorkspaces =
dashboard.experiments.includes("workspaces_batch_actions") || entitlements.features["workspace_batch_actions"].enabled
process.env.NODE_ENV === "development"
// We want to uncheck the selected workspaces always when the url changes // We want to uncheck the selected workspaces always when the url changes
// because of filtering or pagination // because of filtering or pagination
@ -86,9 +85,9 @@ const WorkspacesPage: FC = () => {
</Helmet> </Helmet>
<WorkspacesPageView <WorkspacesPageView
isWorkspaceBatchActionsEnabled={isWorkspaceBatchActionsEnabled}
checkedWorkspaces={checkedWorkspaces} checkedWorkspaces={checkedWorkspaces}
onCheckChange={setCheckedWorkspaces} onCheckChange={setCheckedWorkspaces}
canCheckWorkspaces={canCheckWorkspaces}
workspaces={data?.workspaces} workspaces={data?.workspaces}
dormantWorkspaces={dormantWorkspaces} dormantWorkspaces={dormantWorkspaces}
error={error} error={error}
@ -198,7 +197,7 @@ const BatchDeleteConfirmation = ({
const confirmDeletion = async () => { const confirmDeletion = async () => {
setConfirmError(false) setConfirmError(false)
if (confirmValue.toLowerCase() !== "delete") { if (confirmValue !== "DELETE") {
setConfirmError(true) setConfirmError(true)
return return
} }

View File

@ -97,6 +97,7 @@ const meta: Meta<typeof WorkspacesPageView> = {
limit: DEFAULT_RECORDS_PER_PAGE, limit: DEFAULT_RECORDS_PER_PAGE,
filterProps: defaultFilterProps, filterProps: defaultFilterProps,
checkedWorkspaces: [], checkedWorkspaces: [],
canCheckWorkspaces: true,
}, },
decorators: [ decorators: [
(Story) => ( (Story) => (

View File

@ -44,11 +44,11 @@ export interface WorkspacesPageViewProps {
filterProps: ComponentProps<typeof WorkspacesFilter> filterProps: ComponentProps<typeof WorkspacesFilter>
page: number page: number
limit: number limit: number
isWorkspaceBatchActionsEnabled?: boolean
onPageChange: (page: number) => void onPageChange: (page: number) => void
onUpdateWorkspace: (workspace: Workspace) => void onUpdateWorkspace: (workspace: Workspace) => void
onCheckChange: (checkedWorkspaces: Workspace[]) => void onCheckChange: (checkedWorkspaces: Workspace[]) => void
onDeleteAll: () => void onDeleteAll: () => void
canCheckWorkspaces: boolean
} }
export const WorkspacesPageView: FC< export const WorkspacesPageView: FC<
@ -64,9 +64,9 @@ export const WorkspacesPageView: FC<
onUpdateWorkspace, onUpdateWorkspace,
page, page,
checkedWorkspaces, checkedWorkspaces,
isWorkspaceBatchActionsEnabled,
onCheckChange, onCheckChange,
onDeleteAll, onDeleteAll,
canCheckWorkspaces,
}) => { }) => {
const { saveLocal } = useLocalStorage() const { saveLocal } = useLocalStorage()
@ -131,7 +131,7 @@ export const WorkspacesPageView: FC<
startIcon={<DeleteOutlined />} startIcon={<DeleteOutlined />}
onClick={onDeleteAll} onClick={onDeleteAll}
> >
Delete all Delete selected
</Button> </Button>
</Box> </Box>
</> </>
@ -151,7 +151,7 @@ export const WorkspacesPageView: FC<
onUpdateWorkspace={onUpdateWorkspace} onUpdateWorkspace={onUpdateWorkspace}
checkedWorkspaces={checkedWorkspaces} checkedWorkspaces={checkedWorkspaces}
onCheckChange={onCheckChange} onCheckChange={onCheckChange}
isWorkspaceBatchActionsEnabled={isWorkspaceBatchActionsEnabled} canCheckWorkspaces={canCheckWorkspaces}
/> />
{count !== undefined && ( {count !== undefined && (
<PaginationWidgetBase <PaginationWidgetBase

View File

@ -8,7 +8,10 @@ import { Workspace } from "api/typesGenerated"
import { FC, ReactNode } from "react" import { FC, ReactNode } from "react"
import { TableEmpty } from "components/TableEmpty/TableEmpty" import { TableEmpty } from "components/TableEmpty/TableEmpty"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { TableLoaderSkeleton } from "components/TableLoader/TableLoader" import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader"
import AddOutlined from "@mui/icons-material/AddOutlined" import AddOutlined from "@mui/icons-material/AddOutlined"
import Button from "@mui/material/Button" import Button from "@mui/material/Button"
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
@ -32,24 +35,26 @@ import { WorkspaceOutdatedTooltip } from "components/Tooltips"
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
import { getDisplayWorkspaceTemplateName } from "utils/workspace" import { getDisplayWorkspaceTemplateName } from "utils/workspace"
import Checkbox from "@mui/material/Checkbox" import Checkbox from "@mui/material/Checkbox"
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"
import Skeleton from "@mui/material/Skeleton"
export interface WorkspacesTableProps { export interface WorkspacesTableProps {
workspaces?: Workspace[] workspaces?: Workspace[]
checkedWorkspaces: Workspace[] checkedWorkspaces: Workspace[]
error?: unknown error?: unknown
isUsingFilter: boolean isUsingFilter: boolean
isWorkspaceBatchActionsEnabled?: boolean
onUpdateWorkspace: (workspace: Workspace) => void onUpdateWorkspace: (workspace: Workspace) => void
onCheckChange: (checkedWorkspaces: Workspace[]) => void onCheckChange: (checkedWorkspaces: Workspace[]) => void
canCheckWorkspaces: boolean
} }
export const WorkspacesTable: FC<WorkspacesTableProps> = ({ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
workspaces, workspaces,
checkedWorkspaces, checkedWorkspaces,
isUsingFilter, isUsingFilter,
isWorkspaceBatchActionsEnabled,
onUpdateWorkspace, onUpdateWorkspace,
onCheckChange, onCheckChange,
canCheckWorkspaces,
}) => { }) => {
const { t } = useTranslation("workspacesPage") const { t } = useTranslation("workspacesPage")
const styles = useStyles() const styles = useStyles()
@ -59,15 +64,13 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
{isWorkspaceBatchActionsEnabled ? ( <TableCell width="40%">
<TableCell <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
width="40%" {canCheckWorkspaces && (
sx={{
paddingLeft: (theme) => `${theme.spacing(1.5)} !important`,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Checkbox <Checkbox
// Remove the extra padding added for the first cell in the
// table
sx={{ marginLeft: "-20px" }}
disabled={!workspaces || workspaces.length === 0} disabled={!workspaces || workspaces.length === 0}
checked={checkedWorkspaces.length === workspaces?.length} checked={checkedWorkspaces.length === workspaces?.length}
size="small" size="small"
@ -83,13 +86,10 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
} }
}} }}
/> />
Name )}
</Box> Name
</TableCell> </Box>
) : ( </TableCell>
<TableCell width="40%">Name</TableCell>
)}
<TableCell width="25%">Template</TableCell> <TableCell width="25%">Template</TableCell>
<TableCell width="20%">Last used</TableCell> <TableCell width="20%">Last used</TableCell>
<TableCell width="15%">Status</TableCell> <TableCell width="15%">Status</TableCell>
@ -97,7 +97,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{!workspaces && <TableLoaderSkeleton columns={5} useAvatarData />} {!workspaces && <TableLoader />}
{workspaces && workspaces.length === 0 && ( {workspaces && workspaces.length === 0 && (
<ChooseOne> <ChooseOne>
<Cond condition={isUsingFilter}> <Cond condition={isUsingFilter}>
@ -139,17 +139,13 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
key={workspace.id} key={workspace.id}
checked={checked} checked={checked}
> >
<TableCell <TableCell>
sx={{
paddingLeft: (theme) =>
isWorkspaceBatchActionsEnabled
? `${theme.spacing(1.5)} !important`
: undefined,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{isWorkspaceBatchActionsEnabled && ( {canCheckWorkspaces && (
<Checkbox <Checkbox
// Remove the extra padding added for the first cell in the
// table
sx={{ marginLeft: "-20px" }}
data-testid={`checkbox-${workspace.id}`} data-testid={`checkbox-${workspace.id}`}
size="small" size="small"
disabled={cantBeChecked(workspace)} disabled={cantBeChecked(workspace)}
@ -289,6 +285,38 @@ export const UnhealthyTooltip = () => {
) )
} }
const TableLoader = () => {
return (
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell
width="40%"
sx={{
paddingLeft: (theme) => `${theme.spacing(1.5)} !important`,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Checkbox size="small" disabled />
<AvatarDataSkeleton />
</Box>
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
</TableRowSkeleton>
</TableLoaderSkeleton>
)
}
const cantBeChecked = (workspace: Workspace) => { const cantBeChecked = (workspace: Workspace) => {
return ["deleting", "pending"].includes(workspace.latest_build.status) return ["deleting", "pending"].includes(workspace.latest_build.status)
} }

View File

@ -1757,7 +1757,12 @@ export const MockEntitlements: TypesGen.Entitlements = {
errors: [], errors: [],
warnings: [], warnings: [],
has_license: false, has_license: false,
features: withDefaultFeatures({}), features: withDefaultFeatures({
workspace_batch_actions: {
enabled: true,
entitlement: "entitled",
},
}),
require_telemetry: false, require_telemetry: false,
trial: false, trial: false,
refreshed_at: "2022-05-20T16:45:57.122Z", refreshed_at: "2022-05-20T16:45:57.122Z",
@ -1821,7 +1826,6 @@ export const MockEntitlementsWithScheduling: TypesGen.Entitlements = {
export const MockExperiments: TypesGen.Experiment[] = [ export const MockExperiments: TypesGen.Experiment[] = [
"workspace_actions", "workspace_actions",
"moons", "moons",
"workspaces_batch_actions",
] ]
export const MockAuditLog: TypesGen.AuditLog = { export const MockAuditLog: TypesGen.AuditLog = {