mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -47,7 +47,6 @@ type TasksTableProps = {
|
||||
onRetry: () => void;
|
||||
checkedTaskIds?: Set<string>;
|
||||
onCheckChange?: (checkedTaskIds: Set<string>) => void;
|
||||
canCheckTasks?: boolean;
|
||||
};
|
||||
|
||||
export const TasksTable: FC<TasksTableProps> = ({
|
||||
@@ -56,14 +55,13 @@ export const TasksTable: FC<TasksTableProps> = ({
|
||||
onRetry,
|
||||
checkedTaskIds = new Set(),
|
||||
onCheckChange,
|
||||
canCheckTasks = false,
|
||||
}) => {
|
||||
let body: ReactNode = null;
|
||||
|
||||
if (error) {
|
||||
body = <TasksErrorBody error={error} onRetry={onRetry} />;
|
||||
} else if (!tasks) {
|
||||
body = <TasksSkeleton canCheckTasks={canCheckTasks} />;
|
||||
body = <TasksSkeleton />;
|
||||
} else if (tasks.length === 0) {
|
||||
body = <TasksEmpty />;
|
||||
} else {
|
||||
@@ -84,7 +82,6 @@ export const TasksTable: FC<TasksTableProps> = ({
|
||||
}
|
||||
onCheckChange(newIds);
|
||||
}}
|
||||
canCheck={canCheckTasks}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -96,28 +93,26 @@ export const TasksTable: FC<TasksTableProps> = ({
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheckTasks && (
|
||||
<Checkbox
|
||||
disabled={!tasks || tasks.length === 0}
|
||||
checked={
|
||||
tasks &&
|
||||
tasks.length > 0 &&
|
||||
checkedTaskIds.size === tasks.length
|
||||
<Checkbox
|
||||
disabled={!tasks || tasks.length === 0}
|
||||
checked={
|
||||
tasks &&
|
||||
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
|
||||
</div>
|
||||
</TableHead>
|
||||
@@ -182,15 +177,9 @@ type TaskRowProps = {
|
||||
task: Task;
|
||||
checked: boolean;
|
||||
onCheckChange: (taskId: string, checked: boolean) => void;
|
||||
canCheck: boolean;
|
||||
};
|
||||
|
||||
const TaskRow: FC<TaskRowProps> = ({
|
||||
task,
|
||||
checked,
|
||||
onCheckChange,
|
||||
canCheck,
|
||||
}) => {
|
||||
const TaskRow: FC<TaskRowProps> = ({ 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<TaskRowProps> = ({
|
||||
>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheck && (
|
||||
<Checkbox
|
||||
data-testid={`checkbox-${task.id}`}
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onCheckedChange={(checked) => {
|
||||
onCheckChange(task.id, Boolean(checked));
|
||||
}}
|
||||
aria-label={`Select task ${task.initial_prompt}`}
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
data-testid={`checkbox-${task.id}`}
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onCheckedChange={(checked) => {
|
||||
onCheckChange(task.id, Boolean(checked));
|
||||
}}
|
||||
aria-label={`Select task ${task.initial_prompt}`}
|
||||
/>
|
||||
<AvatarData
|
||||
title={
|
||||
<span className="block max-w-[520px] truncate">
|
||||
@@ -333,17 +320,13 @@ const TaskRow: FC<TaskRowProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
type TasksSkeletonProps = {
|
||||
canCheckTasks: boolean;
|
||||
};
|
||||
|
||||
const TasksSkeleton: FC<TasksSkeletonProps> = ({ canCheckTasks }) => {
|
||||
const TasksSkeleton: FC = () => {
|
||||
return (
|
||||
<TableLoaderSkeleton>
|
||||
<TableRowSkeleton>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheckTasks && <Checkbox disabled />}
|
||||
<Checkbox disabled />
|
||||
<AvatarDataSkeleton />
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -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<BatchAction>();
|
||||
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}
|
||||
|
||||
@@ -168,7 +168,6 @@ const meta: Meta<typeof WorkspacesPageView> = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<WorkspacesPageViewProps> = ({
|
||||
onBatchStopTransition,
|
||||
onBatchStartTransition,
|
||||
isRunningBatchAction,
|
||||
canCheckWorkspaces,
|
||||
templates,
|
||||
templatesFetchStatus,
|
||||
canCreateTemplate,
|
||||
@@ -231,7 +229,6 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
isUsingFilter={filterState.filter.used}
|
||||
checkedWorkspaces={checkedWorkspaces}
|
||||
onCheckChange={onCheckChange}
|
||||
canCheckWorkspaces={canCheckWorkspaces}
|
||||
templates={templates}
|
||||
onActionSuccess={onActionSuccess}
|
||||
onActionError={onActionError}
|
||||
|
||||
@@ -92,7 +92,6 @@ interface WorkspacesTableProps {
|
||||
error?: unknown;
|
||||
isUsingFilter: boolean;
|
||||
onCheckChange: (checkedWorkspaces: readonly Workspace[]) => void;
|
||||
canCheckWorkspaces: boolean;
|
||||
templates?: Template[];
|
||||
canCreateTemplate: boolean;
|
||||
onActionSuccess: () => Promise<void>;
|
||||
@@ -104,7 +103,6 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
checkedWorkspaces,
|
||||
isUsingFilter,
|
||||
onCheckChange,
|
||||
canCheckWorkspaces,
|
||||
templates,
|
||||
canCreateTemplate,
|
||||
onActionSuccess,
|
||||
@@ -118,28 +116,26 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheckWorkspaces && (
|
||||
<Checkbox
|
||||
disabled={!workspaces || workspaces.length === 0}
|
||||
checked={
|
||||
workspaces &&
|
||||
workspaces.length > 0 &&
|
||||
checkedWorkspaces.length === workspaces.length
|
||||
<Checkbox
|
||||
disabled={!workspaces || workspaces.length === 0}
|
||||
checked={
|
||||
workspaces &&
|
||||
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
|
||||
</div>
|
||||
</TableHead>
|
||||
@@ -151,7 +147,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="[&_td]:h-[72px]">
|
||||
{!workspaces && <TableLoader canCheckWorkspaces={canCheckWorkspaces} />}
|
||||
{!workspaces && <TableLoader />}
|
||||
{workspaces && workspaces.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
@@ -177,28 +173,26 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheckWorkspaces && (
|
||||
<Checkbox
|
||||
data-testid={`checkbox-${workspace.id}`}
|
||||
disabled={cantBeChecked(workspace)}
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
onCheckChange([...checkedWorkspaces, workspace]);
|
||||
} else {
|
||||
onCheckChange(
|
||||
checkedWorkspaces.filter(
|
||||
(w) => w.id !== workspace.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
aria-label={`Select workspace ${workspace.name}`}
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
data-testid={`checkbox-${workspace.id}`}
|
||||
disabled={cantBeChecked(workspace)}
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
onCheckChange([...checkedWorkspaces, workspace]);
|
||||
} else {
|
||||
onCheckChange(
|
||||
checkedWorkspaces.filter(
|
||||
(w) => w.id !== workspace.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
aria-label={`Select workspace ${workspace.name}`}
|
||||
/>
|
||||
<AvatarData
|
||||
title={
|
||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||
@@ -333,17 +327,13 @@ const WorkspacesRow: FC<WorkspacesRowProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface TableLoaderProps {
|
||||
canCheckWorkspaces?: boolean;
|
||||
}
|
||||
|
||||
const TableLoader: FC<TableLoaderProps> = ({ canCheckWorkspaces }) => {
|
||||
const TableLoader: FC = () => {
|
||||
return (
|
||||
<TableLoaderSkeleton>
|
||||
<TableRowSkeleton>
|
||||
<TableCell className="w-2/6">
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheckWorkspaces && <Checkbox disabled />}
|
||||
<Checkbox disabled />
|
||||
<AvatarDataSkeleton />
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user