diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 9a156fd3131b3..90c3b3865b224 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -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": {
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 83b0ccb481940..6bcce9d2da651 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -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": {
diff --git a/codersdk/deployment.go b/codersdk/deployment.go
index 75fca76863eef..eda1f615766a5 100644
--- a/codersdk/deployment.go
+++ b/codersdk/deployment.go
@@ -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.
@@ -1941,9 +1943,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"
)
@@ -1954,7 +1953,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.
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index c004bca3e5dec..d331772814d28 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -2719,7 +2719,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
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 0df30b3ab14b5..3b0bc68d915f8 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -1605,7 +1605,6 @@ export type Experiment =
| "tailnet_pg_coordinator"
| "template_autostop_requirement"
| "workspace_actions"
- | "workspaces_batch_actions"
export const Experiments: Experiment[] = [
"deployment_health_page",
"moons",
@@ -1613,7 +1612,6 @@ export const Experiments: Experiment[] = [
"tailnet_pg_coordinator",
"template_autostop_requirement",
"workspace_actions",
- "workspaces_batch_actions",
]
// From codersdk/deployment.go
@@ -1630,6 +1628,7 @@ export type FeatureName =
| "template_rbac"
| "user_limit"
| "user_role_management"
+ | "workspace_batch_actions"
| "workspace_proxy"
export const FeatureNames: FeatureName[] = [
"advanced_template_scheduling",
@@ -1644,6 +1643,7 @@ export const FeatureNames: FeatureName[] = [
"template_rbac",
"user_limit",
"user_role_management",
+ "workspace_batch_actions",
"workspace_proxy",
]
diff --git a/site/src/components/TableLoader/TableLoader.tsx b/site/src/components/TableLoader/TableLoader.tsx
index 9c569f08b208f..70851ce61af03 100644
--- a/site/src/components/TableLoader/TableLoader.tsx
+++ b/site/src/components/TableLoader/TableLoader.tsx
@@ -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)
-
+ children: ReactNode
+}) => {
+ if (!isValidElement(children)) {
+ throw new Error(
+ "TableLoaderSkeleton children must be a valid React element",
+ )
+ }
return (
<>
- {placeholderRows.map((_, rowIndex) => (
-
- {placeholderColumns.map((_, columnIndex) => {
- if (useAvatarData && columnIndex === 0) {
- return (
-
-
-
- )
- }
-
- return (
-
-
-
- )
- })}
-
- ))}
+ {Array.from({ length: rows }, (_, i) =>
+ cloneElement(children, { key: i }),
+ )}
>
)
}
+
+export const TableRowSkeleton = (props: TableRowProps) => {
+ return
+}
diff --git a/site/src/components/UsersTable/UsersTableBody.tsx b/site/src/components/UsersTable/UsersTableBody.tsx
index 73abfd639a273..c5bcae9c147ee 100644
--- a/site/src/components/UsersTable/UsersTableBody.tsx
+++ b/site/src/components/UsersTable/UsersTableBody.tsx
@@ -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 (
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {canEditUsers && (
+
+
+
+ )}
+
+
diff --git a/site/src/pages/GroupsPage/GroupsPageView.tsx b/site/src/pages/GroupsPage/GroupsPageView.tsx
index 1acb5e79cf769..5cac6e3bce238 100644
--- a/site/src/pages/GroupsPage/GroupsPageView.tsx
+++ b/site/src/pages/GroupsPage/GroupsPageView.tsx
@@ -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 = ({
-
+
@@ -184,6 +190,26 @@ export const GroupsPageView: FC = ({
)
}
+const TableLoader = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
const useStyles = makeStyles((theme) => ({
clickableTableRow: {
cursor: "pointer",
diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx
index 77a8edad916a1..4aad4c16b6603 100644
--- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx
+++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx
@@ -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<
-
+
@@ -222,6 +228,32 @@ export const TemplatesPageView: FC<
)
}
+const TableLoader = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
const useStyles = makeStyles((theme) => ({
templateIconWrapper: {
// Same size then the avatar component
diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
index 6abb3d06e2310..9b03d98d80d0f 100644
--- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
+++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx
@@ -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"))
diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx
index 9684e156c8ce7..ad3bf1d00aa6b 100644
--- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx
+++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx
@@ -68,10 +68,9 @@ const WorkspacesPage: FC = () => {
const [checkedWorkspaces, setCheckedWorkspaces] = useState([])
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 = () => {
{
setConfirmError(false)
- if (confirmValue.toLowerCase() !== "delete") {
+ if (confirmValue !== "DELETE") {
setConfirmError(true)
return
}
diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx
index f688cf3508ce1..42f9b83e5ad27 100644
--- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx
+++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx
@@ -97,6 +97,7 @@ const meta: Meta = {
limit: DEFAULT_RECORDS_PER_PAGE,
filterProps: defaultFilterProps,
checkedWorkspaces: [],
+ canCheckWorkspaces: true,
},
decorators: [
(Story) => (
diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
index cb3a8cd2f2e62..16563d1a0235c 100644
--- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
+++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
@@ -44,11 +44,11 @@ export interface WorkspacesPageViewProps {
filterProps: ComponentProps
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={}
onClick={onDeleteAll}
>
- Delete all
+ Delete selected
>
@@ -151,7 +151,7 @@ export const WorkspacesPageView: FC<
onUpdateWorkspace={onUpdateWorkspace}
checkedWorkspaces={checkedWorkspaces}
onCheckChange={onCheckChange}
- isWorkspaceBatchActionsEnabled={isWorkspaceBatchActionsEnabled}
+ canCheckWorkspaces={canCheckWorkspaces}
/>
{count !== undefined && (
void
onCheckChange: (checkedWorkspaces: Workspace[]) => void
+ canCheckWorkspaces: boolean
}
export const WorkspacesTable: FC = ({
workspaces,
checkedWorkspaces,
isUsingFilter,
- isWorkspaceBatchActionsEnabled,
onUpdateWorkspace,
onCheckChange,
+ canCheckWorkspaces,
}) => {
const { t } = useTranslation("workspacesPage")
const styles = useStyles()
@@ -59,15 +64,13 @@ export const WorkspacesTable: FC = ({
- {isWorkspaceBatchActionsEnabled ? (
- `${theme.spacing(1.5)} !important`,
- }}
- >
-
+
+
+ {canCheckWorkspaces && (
= ({
}
}}
/>
- Name
-
-
- ) : (
- Name
- )}
-
+ )}
+ Name
+
+
Template
Last used
Status
@@ -97,7 +97,7 @@ export const WorkspacesTable: FC = ({
- {!workspaces && }
+ {!workspaces && }
{workspaces && workspaces.length === 0 && (
@@ -139,17 +139,13 @@ export const WorkspacesTable: FC = ({
key={workspace.id}
checked={checked}
>
-
- isWorkspaceBatchActionsEnabled
- ? `${theme.spacing(1.5)} !important`
- : undefined,
- }}
- >
+
- {isWorkspaceBatchActionsEnabled && (
+ {canCheckWorkspaces && (
{
)
}
+const TableLoader = () => {
+ return (
+
+
+ `${theme.spacing(1.5)} !important`,
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
const cantBeChecked = (workspace: Workspace) => {
return ["deleting", "pending"].includes(workspace.latest_build.status)
}
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index 78ff1537c525d..3c34379864910 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -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 = {