diff --git a/src/web/entities/BulkTags.jsx b/src/web/entities/BulkTags.jsx
new file mode 100644
index 0000000000..42e406d7da
--- /dev/null
+++ b/src/web/entities/BulkTags.jsx
@@ -0,0 +1,236 @@
+/* SPDX-FileCopyrightText: 2024 Greenbone AG
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import React, {useCallback, useEffect, useState} from 'react';
+
+import _ from 'gmp/locale';
+import {YES_VALUE} from 'gmp/parser';
+import {isDefined} from 'gmp/utils/identity';
+import {apiType, getEntityType, typeName} from 'gmp/utils/entitytype';
+
+import PropTypes from 'web/utils/proptypes';
+import SelectionType from 'web/utils/selectiontype';
+
+import useGmp from 'web/utils/useGmp';
+import useUserSessionTimeout from 'web/utils/useUserSessionTimeout';
+
+import TagDialog from 'web/pages/tags/dialog';
+
+import TagsDialog from './tagsdialog';
+
+const getEntityIds = (entityArray = []) => entityArray.map(entity => entity.id);
+
+const getMultiTagEntitiesCount = (
+ pageEntities,
+ counts,
+ selectedEntities,
+ selectionType,
+) => {
+ if (selectionType === SelectionType.SELECTION_USER) {
+ // support set and array
+ return isDefined(selectedEntities?.size)
+ ? selectedEntities.size
+ : selectedEntities.length;
+ }
+
+ if (selectionType === SelectionType.SELECTION_PAGE_CONTENTS) {
+ return pageEntities.length;
+ }
+
+ return counts.filtered;
+};
+
+const BulkTags = ({
+ entities,
+ selectedEntities,
+ filter,
+ selectionType,
+ entitiesCounts,
+ onClose,
+}) => {
+ const gmp = useGmp();
+ const [, renewSession] = useUserSessionTimeout();
+ const [tag, setTag] = useState({});
+ const [tagDialogVisible, setTagDialogVisible] = useState(false);
+ const [tags, setTags] = useState([]);
+ const [error, setError] = useState();
+
+ const entitiesType = getEntityType(entities[0]);
+ // if there are no entities, BulkTagComponent is not rendered.
+
+ const getTagsByType = useCallback(() => {
+ const tagFilter = `resource_type=${apiType(entitiesType)}`;
+
+ return gmp.tags
+ .getAll({filter: tagFilter})
+ .then(resp => {
+ setTags(resp.data);
+ })
+ .catch(setError);
+ }, [gmp.tags, entitiesType]);
+
+ useEffect(() => {
+ getTagsByType();
+ }, [getTagsByType]);
+
+ const multiTagEntitiesCount = getMultiTagEntitiesCount(
+ entities,
+ entitiesCounts,
+ selectedEntities,
+ selectionType,
+ );
+
+ const closeTagDialog = useCallback(() => {
+ setTagDialogVisible(false);
+ }, []);
+
+ const openTagDialog = useCallback(() => {
+ renewSession();
+ setTagDialogVisible(true);
+ }, [renewSession]);
+
+ const handleCreateTag = useCallback(
+ data => {
+ renewSession();
+
+ return gmp.tag
+ .create(data)
+ .then(response => gmp.tag.get(response.data))
+ .then(response => {
+ const newTags = [...tags, response.data];
+ setTags(newTags);
+ setTag(response.data);
+ })
+ .then(closeTagDialog)
+ .catch(setError);
+ },
+ [closeTagDialog, gmp.tag, renewSession, tags],
+ );
+
+ const handleCloseTagDialog = useCallback(() => {
+ closeTagDialog();
+ renewSession();
+ }, [closeTagDialog, renewSession]);
+
+ const handleTagChange = useCallback(
+ id => {
+ renewSession();
+ return gmp.tag
+ .get({id})
+ .then(resp => {
+ setTag(resp.data);
+ })
+ .catch(setError);
+ },
+ [renewSession, gmp.tag],
+ );
+
+ const handleCloseTagsDialog = useCallback(() => {
+ onClose();
+ }, [onClose]);
+
+ const handleErrorClose = useCallback(() => {
+ setError();
+ }, []);
+
+ const handleAddMultiTag = useCallback(
+ ({comment, id, name, value = ''}) => {
+ let tagEntitiesIds;
+ let loadedFilter;
+
+ if (selectionType === SelectionType.SELECTION_USER) {
+ tagEntitiesIds = getEntityIds(selectedEntities);
+ loadedFilter = null;
+ } else if (selectionType === SelectionType.SELECTION_PAGE_CONTENTS) {
+ tagEntitiesIds = getEntityIds(entities);
+ loadedFilter = null;
+ } else {
+ loadedFilter = filter.all().toFilterString();
+ tagEntitiesIds = null;
+ }
+
+ return gmp.tag
+ .save({
+ active: YES_VALUE,
+ comment,
+ filter: loadedFilter,
+ id,
+ name,
+ resource_ids: tagEntitiesIds,
+ resource_type: entitiesType,
+ resources_action: 'add',
+ value,
+ })
+ .then(onClose)
+ .catch(setError);
+ },
+ [
+ entities,
+ entitiesType,
+ filter,
+ gmp.tag,
+ onClose,
+ selectedEntities,
+ selectionType,
+ ],
+ );
+
+ const resourceTypes = [[entitiesType, typeName(entitiesType)]];
+
+ let title;
+ if (selectionType === SelectionType.SELECTION_USER) {
+ title = _('Add Tag to Selection');
+ } else if (selectionType === SelectionType.SELECTION_PAGE_CONTENTS) {
+ title = _('Add Tag to Page Contents');
+ } else {
+ title = _('Add Tag to All Filtered');
+ }
+
+ return (
+ <>
+
+ {tagDialogVisible && (
+
+ )}
+ >
+ );
+};
+
+BulkTags.propTypes = {
+ entities: PropTypes.arrayOf(PropTypes.model).isRequired,
+ entitiesCounts: PropTypes.counts.isRequired,
+ filter: PropTypes.filter.isRequired,
+ selectedEntities: PropTypes.arrayOf(PropTypes.model).isRequired,
+ selectionType: PropTypes.oneOf([
+ SelectionType.SELECTION_PAGE_CONTENTS,
+ SelectionType.SELECTION_USER,
+ SelectionType.SELECTION_FILTER,
+ ]).isRequired,
+ onClose: PropTypes.func.isRequired,
+};
+
+export default BulkTags;
diff --git a/src/web/entities/__tests__/BulkTags.jsx b/src/web/entities/__tests__/BulkTags.jsx
new file mode 100644
index 0000000000..4aec9f2ddd
--- /dev/null
+++ b/src/web/entities/__tests__/BulkTags.jsx
@@ -0,0 +1,192 @@
+/* SPDX-FileCopyrightText: 2024 Greenbone AG
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {describe, test, expect, testing} from '@gsa/testing';
+
+import {
+ screen,
+ rendererWith,
+ getByTestId,
+ fireEvent,
+ getAllByTitle,
+ wait,
+} from 'web/utils/testing';
+
+import {setSessionTimeout} from 'web/store/usersettings/actions';
+
+import {isDefined} from 'gmp/utils/identity';
+
+import date from 'gmp/models/date';
+import Filter from 'gmp/models/filter';
+import Tag from 'gmp/models/tag';
+import Task from 'gmp/models/task';
+
+import SelectionType from 'web/utils/selectiontype';
+
+import BulkTags from '../BulkTags';
+
+const getDialog = () => {
+ return screen.getByRole('dialog');
+};
+
+const getDialogTitle = dialog => {
+ dialog = isDefined(dialog) ? dialog : getDialog();
+ return getByTestId(dialog, 'dialog-title-bar');
+};
+
+const getDialogSaveButton = dialog => {
+ dialog = isDefined(dialog) ? dialog : getDialog();
+ return getByTestId(dialog, 'dialog-save-button');
+};
+
+describe('BulkTags', () => {
+ test('should render the BulkTags component', () => {
+ const entities = [Task.fromElement({id: 1}), Task.fromElement({id: 2})];
+ const entitiesCounts = {filtered: 2, all: 2};
+ const filter = Filter.fromString('');
+ const selectedEntities = [];
+ const onClose = testing.fn();
+ const getAllTags = testing
+ .fn()
+ .mockResolvedValue({data: [Tag.fromElement({id: 1})]});
+ const gmp = {tags: {getAll: getAllTags}};
+ const timeout = date('2019-10-10');
+
+ const {render, store} = rendererWith({gmp, store: true});
+
+ store.dispatch(setSessionTimeout(timeout));
+
+ render(
+ ,
+ );
+ const dialog = getDialog();
+ expect(dialog).toBeInTheDocument();
+ });
+
+ test('should allow to tag all filtered entities', () => {
+ const entities = [Task.fromElement({id: 1}), Task.fromElement({id: 2})];
+ const entitiesCounts = {filtered: 2, all: 2};
+ const filter = Filter.fromString('');
+ const selectedEntities = [];
+ const onClose = testing.fn();
+ const getAllTags = testing
+ .fn()
+ .mockResolvedValue({data: [Tag.fromElement({id: 1})]});
+ const gmp = {tags: {getAll: getAllTags}};
+ const timeout = date('2019-10-10');
+
+ const {render, store} = rendererWith({gmp, store: true});
+
+ store.dispatch(setSessionTimeout(timeout));
+
+ render(
+ ,
+ );
+ const title = getDialogTitle();
+ expect(title).toHaveTextContent('Add Tag to All Filtered');
+ });
+
+ test('should allow to tag tasks with a new tag', async () => {
+ const entities = [Task.fromElement({id: '1'}), Task.fromElement({id: '2'})];
+ const entitiesCounts = {filtered: 2, all: 2};
+ const filter = Filter.fromString('');
+ const selectedEntities = [];
+ const onClose = testing.fn();
+ const createTag = testing.fn().mockResolvedValue({data: {id: '2'}});
+ const getTag = testing
+ .fn()
+ .mockResolvedValue({data: Tag.fromElement({id: '2'})});
+ const getAllTags = testing
+ .fn()
+ .mockResolvedValue({data: [Tag.fromElement({id: '1'})]});
+ const getAllResourceNames = testing.fn().mockResolvedValue({data: []});
+ const saveTag = testing.fn().mockResolvedValue({data: {id: '2'}});
+ const gmp = {
+ tags: {getAll: getAllTags},
+ resourcenames: {getAll: getAllResourceNames},
+ tag: {
+ create: createTag,
+ get: getTag,
+ save: saveTag,
+ },
+ };
+ const timeout = date('2019-10-10');
+
+ const {render, store} = rendererWith({gmp, store: true});
+
+ store.dispatch(setSessionTimeout(timeout));
+
+ render(
+ ,
+ );
+
+ const dialog = getDialog();
+ const newTag = getAllByTitle(dialog, 'Create a new Tag')[0];
+
+ fireEvent.click(newTag);
+ expect(getAllResourceNames).toHaveBeenCalledWith({
+ resource_type: 'task',
+ });
+
+ const dialogs = screen.queryAllByRole('dialog');
+ expect(dialogs.length).toEqual(2);
+
+ const tagsDialog = dialog[0];
+ const tagDialog = dialogs[1];
+ const saveTagButton = getDialogSaveButton(tagDialog);
+
+ fireEvent.click(saveTagButton);
+
+ await wait();
+
+ expect(createTag).toHaveBeenCalledWith({
+ active: 1,
+ comment: '',
+ name: 'default:unnamed',
+ resource_ids: [],
+ resource_type: 'task',
+ resources: [],
+ value: '',
+ });
+ expect(getTag).toHaveBeenCalledWith({id: '2'});
+
+ const saveTagsButton = getDialogSaveButton(tagsDialog);
+
+ fireEvent.click(saveTagsButton);
+
+ expect(saveTag).toHaveBeenCalledWith({
+ active: 1,
+ comment: '',
+ filter: null,
+ id: '2',
+ name: undefined,
+ resource_ids: ['1', '2'],
+ resource_type: 'task',
+ resources_action: 'add',
+ value: '',
+ });
+ });
+});