From 00713385fbdb151085f5fb1c2e1128b421ace90f Mon Sep 17 00:00:00 2001 From: "blinkagent[bot]" <237617714+blinkagent[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:08:36 +0500 Subject: [PATCH] feat: remove license gate from workspace and task bulk actions (#22090) Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> --- docs/user-guides/workspace-management.md | 6 +- .../src/pages/TasksPage/TasksPage.stories.tsx | 4 - site/src/pages/TasksPage/TasksPage.tsx | 5 - site/src/pages/TasksPage/TasksTable.tsx | 83 +++++++---------- .../pages/WorkspacesPage/WorkspacesPage.tsx | 4 - .../WorkspacesPageView.stories.tsx | 2 - .../WorkspacesPage/WorkspacesPageView.tsx | 3 - .../pages/WorkspacesPage/WorkspacesTable.tsx | 92 +++++++++---------- 8 files changed, 75 insertions(+), 124 deletions(-) diff --git a/docs/user-guides/workspace-management.md b/docs/user-guides/workspace-management.md index 8226c1a7ea..840c5e793d 100644 --- a/docs/user-guides/workspace-management.md +++ b/docs/user-guides/workspace-management.md @@ -102,11 +102,7 @@ manually updated the workspace. ## Bulk operations -> [!NOTE] -> Bulk operations are a Premium feature. -> [Learn more](https://coder.com/pricing#compare-plans). - -Licensed admins may apply bulk operations (update, delete, start, stop) in the +Admins may apply bulk operations (update, delete, start, stop) in the **Workspaces** tab. Select the workspaces you'd like to modify with the checkboxes on the left, then use the top-right **Actions** dropdown to apply the operation. diff --git a/site/src/pages/TasksPage/TasksPage.stories.tsx b/site/src/pages/TasksPage/TasksPage.stories.tsx index 5d1767ef08..4110aadce5 100644 --- a/site/src/pages/TasksPage/TasksPage.stories.tsx +++ b/site/src/pages/TasksPage/TasksPage.stories.tsx @@ -415,7 +415,6 @@ export const ResumeTask: Story = { export const BatchActionsEnabled: Story = { parameters: { - features: ["task_batch_actions"], queries: [ { key: ["tasks", { owner: MockUserOwner.username }], @@ -431,7 +430,6 @@ export const BatchActionsEnabled: Story = { export const BatchActionsSomeSelected: Story = { parameters: { - features: ["task_batch_actions"], queries: [ { key: ["tasks", { owner: MockUserOwner.username }], @@ -458,7 +456,6 @@ export const BatchActionsSomeSelected: Story = { export const BatchActionsAllSelected: Story = { parameters: { - features: ["task_batch_actions"], queries: [ { key: ["tasks", { owner: MockUserOwner.username }], @@ -484,7 +481,6 @@ export const BatchActionsAllSelected: Story = { export const BatchActionsDropdownOpen: Story = { parameters: { - features: ["task_batch_actions"], queries: [ { key: ["tasks", { owner: MockUserOwner.username }], diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index a0fe7f9642..8ec311b088 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -30,7 +30,6 @@ import { TableToolbar } from "components/TableToolbar/TableToolbar"; import { useAuthenticated } from "hooks"; import { useSearchParamsKey } from "hooks/useSearchParamsKey"; import { ChevronDownIcon, TrashIcon } from "lucide-react"; -import { useDashboard } from "modules/dashboard/useDashboard"; import { isTaskNotification, notificationIsDisabled, @@ -102,9 +101,6 @@ const TasksPage: FC = () => { await batchActions.delete(checkedTasks); }; - const { entitlements } = useDashboard(); - const canCheckTasks = entitlements.features.task_batch_actions.enabled; - // Count workspaces that will be deleted with the selected tasks. const workspaceCount = checkedTasks.filter( (t) => t.workspace_id !== null, @@ -277,7 +273,6 @@ const TasksPage: FC = () => { onRetry={tasksQuery.refetch} checkedTaskIds={checkedTaskIds} onCheckChange={handleCheckChange} - canCheckTasks={canCheckTasks} /> )} diff --git a/site/src/pages/TasksPage/TasksTable.tsx b/site/src/pages/TasksPage/TasksTable.tsx index 1d45a599ed..06f05203a0 100644 --- a/site/src/pages/TasksPage/TasksTable.tsx +++ b/site/src/pages/TasksPage/TasksTable.tsx @@ -47,7 +47,6 @@ type TasksTableProps = { onRetry: () => void; checkedTaskIds?: Set; onCheckChange?: (checkedTaskIds: Set) => void; - canCheckTasks?: boolean; }; export const TasksTable: FC = ({ @@ -56,14 +55,13 @@ export const TasksTable: FC = ({ onRetry, checkedTaskIds = new Set(), onCheckChange, - canCheckTasks = false, }) => { let body: ReactNode = null; if (error) { body = ; } else if (!tasks) { - body = ; + body = ; } else if (tasks.length === 0) { body = ; } else { @@ -84,7 +82,6 @@ export const TasksTable: FC = ({ } onCheckChange(newIds); }} - canCheck={canCheckTasks} /> ); }); @@ -96,28 +93,26 @@ export const TasksTable: FC = ({
- {canCheckTasks && ( - 0 && - checkedTaskIds.size === tasks.length + 0 && + checkedTaskIds.size === tasks.length + } + onCheckedChange={(checked) => { + if (!tasks || !onCheckChange) { + return; } - onCheckedChange={(checked) => { - if (!tasks || !onCheckChange) { - return; - } - if (!checked) { - onCheckChange(new Set()); - } else { - onCheckChange(new Set(tasks.map((t) => t.id))); - } - }} - aria-label="Select all tasks" - /> - )} + if (!checked) { + onCheckChange(new Set()); + } else { + onCheckChange(new Set(tasks.map((t) => t.id))); + } + }} + aria-label="Select all tasks" + /> Task
@@ -182,15 +177,9 @@ type TaskRowProps = { task: Task; checked: boolean; onCheckChange: (taskId: string, checked: boolean) => void; - canCheck: boolean; }; -const TaskRow: FC = ({ - task, - checked, - onCheckChange, - canCheck, -}) => { +const TaskRow: FC = ({ task, checked, onCheckChange }) => { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const templateDisplayName = task.template_display_name ?? task.template_name; const navigate = useNavigate(); @@ -228,19 +217,17 @@ const TaskRow: FC = ({ >
- {canCheck && ( - { - e.stopPropagation(); - }} - onCheckedChange={(checked) => { - onCheckChange(task.id, Boolean(checked)); - }} - aria-label={`Select task ${task.initial_prompt}`} - /> - )} + { + e.stopPropagation(); + }} + onCheckedChange={(checked) => { + onCheckChange(task.id, Boolean(checked)); + }} + aria-label={`Select task ${task.initial_prompt}`} + /> @@ -333,17 +320,13 @@ const TaskRow: FC = ({ ); }; -type TasksSkeletonProps = { - canCheckTasks: boolean; -}; - -const TasksSkeleton: FC = ({ canCheckTasks }) => { +const TasksSkeleton: FC = () => { return (
- {canCheckTasks && } +
diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index e565386fdc..7e69a52140 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -70,7 +70,6 @@ const WorkspacesPage: FC = () => { }, }); const { permissions, user: me } = useAuthenticated(); - const { entitlements } = useDashboard(); const templatesQuery = useQuery(templates()); const workspacePermissionsQuery = useQuery( workspacePermissionsByOrganization( @@ -129,8 +128,6 @@ const WorkspacesPage: FC = () => { }); const [activeBatchAction, setActiveBatchAction] = useState(); - const canCheckWorkspaces = - entitlements.features.workspace_batch_actions.enabled; const batchActions = useBatchActions({ onSuccess: async () => { await refetch(); @@ -161,7 +158,6 @@ const WorkspacesPage: FC = () => { return new Set(newIds); }); }} - canCheckWorkspaces={canCheckWorkspaces} templates={filteredTemplates} templatesFetchStatus={templatesQuery.status} workspaces={data?.workspaces} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index ddab6b94ab..c173fe0c68 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -168,7 +168,6 @@ const meta: Meta = { limit: DEFAULT_RECORDS_PER_PAGE, filterState: defaultFilterProps, checkedWorkspaces: [], - canCheckWorkspaces: true, templates: mockTemplates, templatesFetchStatus: "success", count: 13, @@ -402,7 +401,6 @@ export const WithCheckedWorkspaces: Story = { args: { workspaces: allWorkspaces.slice(0, 5), checkedWorkspaces: allWorkspaces.slice(0, 2), - canCheckWorkspaces: true, count: 5, }, }; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index d123c0123e..9832fc008b 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -60,7 +60,6 @@ interface WorkspacesPageViewProps { onBatchUpdateTransition: () => void; onBatchStartTransition: () => void; onBatchStopTransition: () => void; - canCheckWorkspaces: boolean; templatesFetchStatus: TemplateQuery["status"]; templates: TemplateQuery["data"]; canCreateTemplate: boolean; @@ -84,7 +83,6 @@ export const WorkspacesPageView: FC = ({ onBatchStopTransition, onBatchStartTransition, isRunningBatchAction, - canCheckWorkspaces, templates, templatesFetchStatus, canCreateTemplate, @@ -231,7 +229,6 @@ export const WorkspacesPageView: FC = ({ isUsingFilter={filterState.filter.used} checkedWorkspaces={checkedWorkspaces} onCheckChange={onCheckChange} - canCheckWorkspaces={canCheckWorkspaces} templates={templates} onActionSuccess={onActionSuccess} onActionError={onActionError} diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 50dcc8d01d..a375a0512d 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -92,7 +92,6 @@ interface WorkspacesTableProps { error?: unknown; isUsingFilter: boolean; onCheckChange: (checkedWorkspaces: readonly Workspace[]) => void; - canCheckWorkspaces: boolean; templates?: Template[]; canCreateTemplate: boolean; onActionSuccess: () => Promise; @@ -104,7 +103,6 @@ export const WorkspacesTable: FC = ({ checkedWorkspaces, isUsingFilter, onCheckChange, - canCheckWorkspaces, templates, canCreateTemplate, onActionSuccess, @@ -118,28 +116,26 @@ export const WorkspacesTable: FC = ({
- {canCheckWorkspaces && ( - 0 && - checkedWorkspaces.length === workspaces.length + 0 && + checkedWorkspaces.length === workspaces.length + } + onCheckedChange={(checked) => { + if (!workspaces) { + return; } - onCheckedChange={(checked) => { - if (!workspaces) { - return; - } - if (!checked) { - onCheckChange([]); - } else { - onCheckChange(workspaces); - } - }} - aria-label="Select all workspaces" - /> - )} + if (!checked) { + onCheckChange([]); + } else { + onCheckChange(workspaces); + } + }} + aria-label="Select all workspaces" + /> Name
@@ -151,7 +147,7 @@ export const WorkspacesTable: FC = ({
- {!workspaces && } + {!workspaces && } {workspaces && workspaces.length === 0 && ( @@ -177,28 +173,26 @@ export const WorkspacesTable: FC = ({ >
- {canCheckWorkspaces && ( - { - e.stopPropagation(); - }} - onCheckedChange={(checked) => { - if (checked) { - onCheckChange([...checkedWorkspaces, workspace]); - } else { - onCheckChange( - checkedWorkspaces.filter( - (w) => w.id !== workspace.id, - ), - ); - } - }} - aria-label={`Select workspace ${workspace.name}`} - /> - )} + { + e.stopPropagation(); + }} + onCheckedChange={(checked) => { + if (checked) { + onCheckChange([...checkedWorkspaces, workspace]); + } else { + onCheckChange( + checkedWorkspaces.filter( + (w) => w.id !== workspace.id, + ), + ); + } + }} + aria-label={`Select workspace ${workspace.name}`} + /> @@ -333,17 +327,13 @@ const WorkspacesRow: FC = ({ ); }; -interface TableLoaderProps { - canCheckWorkspaces?: boolean; -} - -const TableLoader: FC = ({ canCheckWorkspaces }) => { +const TableLoader: FC = () => { return (
- {canCheckWorkspaces && } +