mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
feat(site): make workspace batch deletion GA (#9313)
This commit is contained in:
6
coderd/apidoc/docs.go
generated
6
coderd/apidoc/docs.go
generated
@ -8125,8 +8125,7 @@ const docTemplate = `{
|
||||
"tailnet_pg_coordinator",
|
||||
"single_tailnet",
|
||||
"template_autostop_requirement",
|
||||
"deployment_health_page",
|
||||
"workspaces_batch_actions"
|
||||
"deployment_health_page"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ExperimentMoons",
|
||||
@ -8134,8 +8133,7 @@ const docTemplate = `{
|
||||
"ExperimentTailnetPGCoordinator",
|
||||
"ExperimentSingleTailnet",
|
||||
"ExperimentTemplateAutostopRequirement",
|
||||
"ExperimentDeploymentHealthPage",
|
||||
"ExperimentWorkspacesBatchActions"
|
||||
"ExperimentDeploymentHealthPage"
|
||||
]
|
||||
},
|
||||
"codersdk.Feature": {
|
||||
|
6
coderd/apidoc/swagger.json
generated
6
coderd/apidoc/swagger.json
generated
@ -7276,8 +7276,7 @@
|
||||
"tailnet_pg_coordinator",
|
||||
"single_tailnet",
|
||||
"template_autostop_requirement",
|
||||
"deployment_health_page",
|
||||
"workspaces_batch_actions"
|
||||
"deployment_health_page"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ExperimentMoons",
|
||||
@ -7285,8 +7284,7 @@
|
||||
"ExperimentTailnetPGCoordinator",
|
||||
"ExperimentSingleTailnet",
|
||||
"ExperimentTemplateAutostopRequirement",
|
||||
"ExperimentDeploymentHealthPage",
|
||||
"ExperimentWorkspacesBatchActions"
|
||||
"ExperimentDeploymentHealthPage"
|
||||
]
|
||||
},
|
||||
"codersdk.Feature": {
|
||||
|
@ -48,6 +48,7 @@ const (
|
||||
FeatureAdvancedTemplateScheduling FeatureName = "advanced_template_scheduling"
|
||||
FeatureTemplateAutostopRequirement FeatureName = "template_autostop_requirement"
|
||||
FeatureWorkspaceProxy FeatureName = "workspace_proxy"
|
||||
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions"
|
||||
)
|
||||
|
||||
// FeatureNames must be kept in-sync with the Feature enum above.
|
||||
@ -64,6 +65,7 @@ var FeatureNames = []FeatureName{
|
||||
FeatureAdvancedTemplateScheduling,
|
||||
FeatureWorkspaceProxy,
|
||||
FeatureUserRoleManagement,
|
||||
FeatureWorkspaceBatchActions,
|
||||
}
|
||||
|
||||
// Humanize returns the feature name in a human-readable format.
|
||||
@ -1958,9 +1960,6 @@ const (
|
||||
// Deployment health page
|
||||
ExperimentDeploymentHealthPage Experiment = "deployment_health_page"
|
||||
|
||||
// Workspaces batch actions
|
||||
ExperimentWorkspacesBatchActions Experiment = "workspaces_batch_actions"
|
||||
|
||||
// Add new experiments here!
|
||||
// ExperimentExample Experiment = "example"
|
||||
)
|
||||
@ -1971,7 +1970,6 @@ const (
|
||||
// not be included here and will be essentially hidden.
|
||||
var ExperimentsAll = Experiments{
|
||||
ExperimentDeploymentHealthPage,
|
||||
ExperimentWorkspacesBatchActions,
|
||||
}
|
||||
|
||||
// Experiments is a list of experiments that are enabled for the deployment.
|
||||
|
1
docs/api/schemas.md
generated
1
docs/api/schemas.md
generated
@ -2721,7 +2721,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| `single_tailnet` |
|
||||
| `template_autostop_requirement` |
|
||||
| `deployment_health_page` |
|
||||
| `workspaces_batch_actions` |
|
||||
|
||||
## codersdk.Feature
|
||||
|
||||
|
4
site/src/api/typesGenerated.ts
generated
4
site/src/api/typesGenerated.ts
generated
@ -1606,7 +1606,6 @@ export type Experiment =
|
||||
| "tailnet_pg_coordinator"
|
||||
| "template_autostop_requirement"
|
||||
| "workspace_actions"
|
||||
| "workspaces_batch_actions"
|
||||
export const Experiments: Experiment[] = [
|
||||
"deployment_health_page",
|
||||
"moons",
|
||||
@ -1614,7 +1613,6 @@ export const Experiments: Experiment[] = [
|
||||
"tailnet_pg_coordinator",
|
||||
"template_autostop_requirement",
|
||||
"workspace_actions",
|
||||
"workspaces_batch_actions",
|
||||
]
|
||||
|
||||
// From codersdk/deployment.go
|
||||
@ -1631,6 +1629,7 @@ export type FeatureName =
|
||||
| "template_rbac"
|
||||
| "user_limit"
|
||||
| "user_role_management"
|
||||
| "workspace_batch_actions"
|
||||
| "workspace_proxy"
|
||||
export const FeatureNames: FeatureName[] = [
|
||||
"advanced_template_scheduling",
|
||||
@ -1645,6 +1644,7 @@ export const FeatureNames: FeatureName[] = [
|
||||
"template_rbac",
|
||||
"user_limit",
|
||||
"user_role_management",
|
||||
"workspace_batch_actions",
|
||||
"workspace_proxy",
|
||||
]
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import TableCell from "@mui/material/TableCell"
|
||||
import TableRow from "@mui/material/TableRow"
|
||||
import Skeleton from "@mui/material/Skeleton"
|
||||
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"
|
||||
import { FC } from "react"
|
||||
import TableRow, { TableRowProps } from "@mui/material/TableRow"
|
||||
import { FC, ReactNode, cloneElement, isValidElement } from "react"
|
||||
import { Loader } from "../Loader/Loader"
|
||||
|
||||
export const TableLoader: FC = () => {
|
||||
@ -25,35 +23,27 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
}))
|
||||
|
||||
export const TableLoaderSkeleton: FC<{
|
||||
columns: number
|
||||
export const TableLoaderSkeleton = ({
|
||||
rows = 4,
|
||||
children,
|
||||
}: {
|
||||
rows?: number
|
||||
useAvatarData?: boolean
|
||||
}> = ({ columns, rows = 4, useAvatarData = false }) => {
|
||||
const placeholderColumns = Array(columns).fill(undefined)
|
||||
const placeholderRows = Array(rows).fill(undefined)
|
||||
|
||||
return (
|
||||
<>
|
||||
{placeholderRows.map((_, rowIndex) => (
|
||||
<TableRow key={rowIndex} role="progressbar" data-testid="loader">
|
||||
{placeholderColumns.map((_, columnIndex) => {
|
||||
if (useAvatarData && columnIndex === 0) {
|
||||
return (
|
||||
<TableCell key={columnIndex}>
|
||||
<AvatarDataSkeleton />
|
||||
</TableCell>
|
||||
children: ReactNode
|
||||
}) => {
|
||||
if (!isValidElement(children)) {
|
||||
throw new Error(
|
||||
"TableLoaderSkeleton children must be a valid React element",
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCell key={columnIndex}>
|
||||
<Skeleton variant="text" width="25%" />
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
<>
|
||||
{Array.from({ length: rows }, (_, i) =>
|
||||
cloneElement(children, { key: i }),
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const TableRowSkeleton = (props: TableRowProps) => {
|
||||
return <TableRow role="progressbar" data-testid="loader" {...props} />
|
||||
}
|
||||
|
@ -10,7 +10,10 @@ import * as TypesGen from "../../api/typesGenerated"
|
||||
import { combineClasses } from "../../utils/combineClasses"
|
||||
import { AvatarData } from "../AvatarData/AvatarData"
|
||||
import { EmptyState } from "../EmptyState/EmptyState"
|
||||
import { TableLoaderSkeleton } from "../TableLoader/TableLoader"
|
||||
import {
|
||||
TableLoaderSkeleton,
|
||||
TableRowSkeleton,
|
||||
} from "../TableLoader/TableLoader"
|
||||
import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
|
||||
import { EditRolesButton } from "components/EditRolesButton/EditRolesButton"
|
||||
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 relativeTime from "dayjs/plugin/relativeTime"
|
||||
import ShieldOutlined from "@mui/icons-material/ShieldOutlined"
|
||||
import Skeleton from "@mui/material/Skeleton"
|
||||
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
@ -91,7 +96,29 @@ export const UsersTableBody: FC<
|
||||
return (
|
||||
<ChooseOne>
|
||||
<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 condition={!users || users.length === 0}>
|
||||
<ChooseOne>
|
||||
|
@ -15,7 +15,10 @@ import { AvatarData } from "components/AvatarData/AvatarData"
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||
import { EmptyState } from "components/EmptyState/EmptyState"
|
||||
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 { FC } from "react"
|
||||
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 { GroupAvatar } from "components/GroupAvatar/GroupAvatar"
|
||||
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 = {
|
||||
groups: Group[] | undefined
|
||||
@ -83,7 +89,7 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
|
||||
<TableBody>
|
||||
<ChooseOne>
|
||||
<Cond condition={isLoading}>
|
||||
<TableLoaderSkeleton columns={3} useAvatarData />
|
||||
<TableLoader />
|
||||
</Cond>
|
||||
|
||||
<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) => ({
|
||||
clickableTableRow: {
|
||||
cursor: "pointer",
|
||||
|
@ -24,7 +24,10 @@ import {
|
||||
PageHeaderTitle,
|
||||
} from "../../components/PageHeader/PageHeader"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { TableLoaderSkeleton } from "../../components/TableLoader/TableLoader"
|
||||
import {
|
||||
TableLoaderSkeleton,
|
||||
TableRowSkeleton,
|
||||
} from "../../components/TableLoader/TableLoader"
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipLink,
|
||||
@ -42,6 +45,9 @@ import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined"
|
||||
import { Avatar } from "components/Avatar/Avatar"
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert"
|
||||
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 = {
|
||||
developerCount: (activeCount: number): string => {
|
||||
@ -196,7 +202,7 @@ export const TemplatesPageView: FC<
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<Maybe condition={isLoading}>
|
||||
<TableLoaderSkeleton columns={5} useAvatarData />
|
||||
<TableLoader />
|
||||
</Maybe>
|
||||
|
||||
<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) => ({
|
||||
templateIconWrapper: {
|
||||
// Same size then the avatar component
|
||||
|
@ -63,7 +63,7 @@ describe("WorkspacesPage", () => {
|
||||
|
||||
await user.click(getWorkspaceCheckbox(workspaces[0]))
|
||||
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.click(screen.getByTestId("confirm-button"))
|
||||
|
||||
|
@ -68,10 +68,9 @@ const WorkspacesPage: FC = () => {
|
||||
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"
|
||||
const { entitlements } = useDashboard()
|
||||
const canCheckWorkspaces =
|
||||
entitlements.features["workspace_batch_actions"].enabled
|
||||
|
||||
// We want to uncheck the selected workspaces always when the url changes
|
||||
// because of filtering or pagination
|
||||
@ -86,9 +85,9 @@ const WorkspacesPage: FC = () => {
|
||||
</Helmet>
|
||||
|
||||
<WorkspacesPageView
|
||||
isWorkspaceBatchActionsEnabled={isWorkspaceBatchActionsEnabled}
|
||||
checkedWorkspaces={checkedWorkspaces}
|
||||
onCheckChange={setCheckedWorkspaces}
|
||||
canCheckWorkspaces={canCheckWorkspaces}
|
||||
workspaces={data?.workspaces}
|
||||
dormantWorkspaces={dormantWorkspaces}
|
||||
error={error}
|
||||
@ -198,7 +197,7 @@ const BatchDeleteConfirmation = ({
|
||||
const confirmDeletion = async () => {
|
||||
setConfirmError(false)
|
||||
|
||||
if (confirmValue.toLowerCase() !== "delete") {
|
||||
if (confirmValue !== "DELETE") {
|
||||
setConfirmError(true)
|
||||
return
|
||||
}
|
||||
|
@ -97,6 +97,7 @@ const meta: Meta<typeof WorkspacesPageView> = {
|
||||
limit: DEFAULT_RECORDS_PER_PAGE,
|
||||
filterProps: defaultFilterProps,
|
||||
checkedWorkspaces: [],
|
||||
canCheckWorkspaces: true,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
@ -44,11 +44,11 @@ export interface WorkspacesPageViewProps {
|
||||
filterProps: ComponentProps<typeof WorkspacesFilter>
|
||||
page: number
|
||||
limit: number
|
||||
isWorkspaceBatchActionsEnabled?: boolean
|
||||
onPageChange: (page: number) => void
|
||||
onUpdateWorkspace: (workspace: Workspace) => void
|
||||
onCheckChange: (checkedWorkspaces: Workspace[]) => void
|
||||
onDeleteAll: () => void
|
||||
canCheckWorkspaces: boolean
|
||||
}
|
||||
|
||||
export const WorkspacesPageView: FC<
|
||||
@ -64,9 +64,9 @@ export const WorkspacesPageView: FC<
|
||||
onUpdateWorkspace,
|
||||
page,
|
||||
checkedWorkspaces,
|
||||
isWorkspaceBatchActionsEnabled,
|
||||
onCheckChange,
|
||||
onDeleteAll,
|
||||
canCheckWorkspaces,
|
||||
}) => {
|
||||
const { saveLocal } = useLocalStorage()
|
||||
|
||||
@ -131,7 +131,7 @@ export const WorkspacesPageView: FC<
|
||||
startIcon={<DeleteOutlined />}
|
||||
onClick={onDeleteAll}
|
||||
>
|
||||
Delete all
|
||||
Delete selected
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
@ -151,7 +151,7 @@ export const WorkspacesPageView: FC<
|
||||
onUpdateWorkspace={onUpdateWorkspace}
|
||||
checkedWorkspaces={checkedWorkspaces}
|
||||
onCheckChange={onCheckChange}
|
||||
isWorkspaceBatchActionsEnabled={isWorkspaceBatchActionsEnabled}
|
||||
canCheckWorkspaces={canCheckWorkspaces}
|
||||
/>
|
||||
{count !== undefined && (
|
||||
<PaginationWidgetBase
|
||||
|
@ -8,7 +8,10 @@ import { Workspace } from "api/typesGenerated"
|
||||
import { FC, ReactNode } from "react"
|
||||
import { TableEmpty } from "components/TableEmpty/TableEmpty"
|
||||
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 Button from "@mui/material/Button"
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||
@ -32,24 +35,26 @@ import { WorkspaceOutdatedTooltip } from "components/Tooltips"
|
||||
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
|
||||
import { getDisplayWorkspaceTemplateName } from "utils/workspace"
|
||||
import Checkbox from "@mui/material/Checkbox"
|
||||
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"
|
||||
import Skeleton from "@mui/material/Skeleton"
|
||||
|
||||
export interface WorkspacesTableProps {
|
||||
workspaces?: Workspace[]
|
||||
checkedWorkspaces: Workspace[]
|
||||
error?: unknown
|
||||
isUsingFilter: boolean
|
||||
isWorkspaceBatchActionsEnabled?: boolean
|
||||
onUpdateWorkspace: (workspace: Workspace) => void
|
||||
onCheckChange: (checkedWorkspaces: Workspace[]) => void
|
||||
canCheckWorkspaces: boolean
|
||||
}
|
||||
|
||||
export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
workspaces,
|
||||
checkedWorkspaces,
|
||||
isUsingFilter,
|
||||
isWorkspaceBatchActionsEnabled,
|
||||
onUpdateWorkspace,
|
||||
onCheckChange,
|
||||
canCheckWorkspaces,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacesPage")
|
||||
const styles = useStyles()
|
||||
@ -59,15 +64,13 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{isWorkspaceBatchActionsEnabled ? (
|
||||
<TableCell
|
||||
width="40%"
|
||||
sx={{
|
||||
paddingLeft: (theme) => `${theme.spacing(1.5)} !important`,
|
||||
}}
|
||||
>
|
||||
<TableCell width="40%">
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
{canCheckWorkspaces && (
|
||||
<Checkbox
|
||||
// Remove the extra padding added for the first cell in the
|
||||
// table
|
||||
sx={{ marginLeft: "-20px" }}
|
||||
disabled={!workspaces || workspaces.length === 0}
|
||||
checked={checkedWorkspaces.length === workspaces?.length}
|
||||
size="small"
|
||||
@ -83,13 +86,10 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
Name
|
||||
</Box>
|
||||
</TableCell>
|
||||
) : (
|
||||
<TableCell width="40%">Name</TableCell>
|
||||
)}
|
||||
|
||||
<TableCell width="25%">Template</TableCell>
|
||||
<TableCell width="20%">Last used</TableCell>
|
||||
<TableCell width="15%">Status</TableCell>
|
||||
@ -97,7 +97,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{!workspaces && <TableLoaderSkeleton columns={5} useAvatarData />}
|
||||
{!workspaces && <TableLoader />}
|
||||
{workspaces && workspaces.length === 0 && (
|
||||
<ChooseOne>
|
||||
<Cond condition={isUsingFilter}>
|
||||
@ -139,17 +139,13 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
key={workspace.id}
|
||||
checked={checked}
|
||||
>
|
||||
<TableCell
|
||||
sx={{
|
||||
paddingLeft: (theme) =>
|
||||
isWorkspaceBatchActionsEnabled
|
||||
? `${theme.spacing(1.5)} !important`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
{isWorkspaceBatchActionsEnabled && (
|
||||
{canCheckWorkspaces && (
|
||||
<Checkbox
|
||||
// Remove the extra padding added for the first cell in the
|
||||
// table
|
||||
sx={{ marginLeft: "-20px" }}
|
||||
data-testid={`checkbox-${workspace.id}`}
|
||||
size="small"
|
||||
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) => {
|
||||
return ["deleting", "pending"].includes(workspace.latest_build.status)
|
||||
}
|
||||
|
@ -1757,7 +1757,12 @@ export const MockEntitlements: TypesGen.Entitlements = {
|
||||
errors: [],
|
||||
warnings: [],
|
||||
has_license: false,
|
||||
features: withDefaultFeatures({}),
|
||||
features: withDefaultFeatures({
|
||||
workspace_batch_actions: {
|
||||
enabled: true,
|
||||
entitlement: "entitled",
|
||||
},
|
||||
}),
|
||||
require_telemetry: false,
|
||||
trial: false,
|
||||
refreshed_at: "2022-05-20T16:45:57.122Z",
|
||||
@ -1821,7 +1826,6 @@ export const MockEntitlementsWithScheduling: TypesGen.Entitlements = {
|
||||
export const MockExperiments: TypesGen.Experiment[] = [
|
||||
"workspace_actions",
|
||||
"moons",
|
||||
"workspaces_batch_actions",
|
||||
]
|
||||
|
||||
export const MockAuditLog: TypesGen.AuditLog = {
|
||||
|
Reference in New Issue
Block a user