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",
|
"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": {
|
||||||
|
6
coderd/apidoc/swagger.json
generated
6
coderd/apidoc/swagger.json
generated
@ -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": {
|
||||||
|
@ -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
1
docs/api/schemas.md
generated
@ -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
|
||||||
|
|
||||||
|
4
site/src/api/typesGenerated.ts
generated
4
site/src/api/typesGenerated.ts
generated
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -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} />
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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"))
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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) => (
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 = {
|
||||||
|
Reference in New Issue
Block a user