diff --git a/.gitignore b/.gitignore
index 01ccfa2..a53b01e 100755
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,7 @@ pip-log.txt
 .tox
 nosetests.xml
 .pytest_cache
+cmdb-api/test-output
 
 # Translations
 *.mo
diff --git a/cmdb-api/api/lib/cmdb/ci_type.py b/cmdb-api/api/lib/cmdb/ci_type.py
index 8c7934d..6a6885a 100644
--- a/cmdb-api/api/lib/cmdb/ci_type.py
+++ b/cmdb-api/api/lib/cmdb/ci_type.py
@@ -23,6 +23,7 @@ from api.models.cmdb import CITypeGroupItem
 from api.models.cmdb import CITypeRelation
 from api.models.cmdb import PreferenceShowAttributes
 from api.models.cmdb import PreferenceTreeView
+from api.tasks.cmdb import ci_type_attribute_order_rebuild
 
 
 class CITypeManager(object):
@@ -39,7 +40,9 @@ class CITypeManager(object):
 
     @staticmethod
     def check_is_existed(key):
-        return CITypeCache.get(key) or abort(404, "CIType <{0}> is not existed".format(key))
+        ci_type = CITypeCache.get(key) or abort(404, "CIType <{0}> is not existed".format(key))
+
+        return CIType.get_by_id(ci_type.id)
 
     @staticmethod
     def get_ci_types(type_name=None):
@@ -55,16 +58,35 @@ class CITypeManager(object):
         ci_type = CITypeCache.get(_type) or abort(404, "CIType <{0}> is not found".format(_type))
         return ci_type.to_dict()
 
+    @staticmethod
+    def _validate_unique(type_id=None, name=None, alias=None):
+        if name is not None:
+            ci_type = CIType.get_by(name=name, first=True, to_dict=False)
+        elif alias is not None:
+            ci_type = CIType.get_by(alias=alias, first=True, to_dict=False)
+        else:
+            return
+
+        if not ci_type:
+            return
+
+        if type_id is not None and ci_type.id != type_id:
+            return abort(400, "CIType <{0}> is already existed".format(name or alias))
+
+        if type_id is None and ci_type is not None:
+            return abort(400, "CIType <{0}> is already existed".format(name or alias))
+
     @classmethod
     @kwargs_required("name")
     def add(cls, **kwargs):
         unique_key = kwargs.pop("unique_key", None)
         unique_key = AttributeCache.get(unique_key) or abort(404, "Unique key is not defined")
 
-        CIType.get_by(name=kwargs['name']) and abort(404, "CIType <{0}> is already existed".format(kwargs.get("name")))
-
         kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"]
 
+        cls._validate_unique(name=kwargs['name'])
+        cls._validate_unique(alias=kwargs['alias'])
+
         kwargs["unique_id"] = unique_key.id
         ci_type = CIType.create(**kwargs)
 
@@ -88,6 +110,9 @@ class CITypeManager(object):
 
         ci_type = cls.check_is_existed(type_id)
 
+        cls._validate_unique(type_id=type_id, name=kwargs.get('name'))
+        cls._validate_unique(type_id=type_id, alias=kwargs.get('alias'))
+
         unique_key = kwargs.pop("unique_key", None)
         unique_key = AttributeCache.get(unique_key)
         if unique_key is not None:
@@ -305,6 +330,31 @@ class CITypeAttributeManager(object):
 
         CITypeAttributesCache.clean(type_id)
 
+    @classmethod
+    def transfer(cls, type_id, _from, _to):
+        current_app.logger.info("[{0}] {1} -> {2}".format(type_id, _from, _to))
+        attr_id = _from.get('attr_id')
+        from_group_id = _from.get('group_id')
+        to_group_id = _to.get('group_id')
+        order = _to.get('order')
+
+        if from_group_id != to_group_id:
+            if from_group_id is not None:
+                CITypeAttributeGroupManager.delete_item(from_group_id, attr_id)
+
+            if to_group_id is not None:
+                CITypeAttributeGroupManager.add_item(to_group_id, attr_id, order)
+
+        elif from_group_id:
+            CITypeAttributeGroupManager.update_item(from_group_id, attr_id, order)
+
+        else:  # other attribute transfer
+            return abort(400, "invalid operation!!!")
+
+        CITypeAttributesCache.clean(type_id)
+
+        ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE)
+
 
 class CITypeRelationManager(object):
     """
@@ -441,6 +491,8 @@ class CITypeAttributeGroupManager(object):
             return abort(400, "Group <{0}> duplicate".format(name))
         if name is not None:
             group.update(name=name)
+        else:
+            name = group.name
 
         cls.create_or_update(group.type_id, name, attr_order, group_order)
 
@@ -455,3 +507,86 @@ class CITypeAttributeGroupManager(object):
             item.soft_delete()
 
         return group_id
+
+    @classmethod
+    def add_item(cls, group_id, attr_id, order):
+        db.session.remove()
+
+        existed = CITypeAttributeGroupItem.get_by(group_id=group_id,
+                                                  attr_id=attr_id,
+                                                  first=True,
+                                                  to_dict=False)
+        if existed is not None:
+            existed.update(order=order)
+        else:
+            CITypeAttributeGroupItem.create(group_id=group_id, attr_id=attr_id, order=order)
+
+        gt_items = db.session.query(CITypeAttributeGroupItem).filter(
+            CITypeAttributeGroupItem.deleted.is_(False)).filter(CITypeAttributeGroupItem.order > order)
+        for _item in gt_items:
+            _order = _item.order
+            _item.update(order=_order + 1)
+
+    @classmethod
+    def update_item(cls, group_id, attr_id, order):
+        db.session.remove()
+
+        existed = CITypeAttributeGroupItem.get_by(group_id=group_id,
+                                                  attr_id=attr_id,
+                                                  first=True,
+                                                  to_dict=False)
+        existed or abort(404, "Group<{0}> - Attribute<{1}> is not found".format(group_id, attr_id))
+
+        if existed.order > order:  # forward, +1
+            items = db.session.query(CITypeAttributeGroupItem).filter(
+                CITypeAttributeGroupItem.deleted.is_(False)).filter(
+                CITypeAttributeGroupItem.order >= order).filter(
+                CITypeAttributeGroupItem.order < existed.order)
+            for item in items:
+                item.update(order=item.order + 1)
+
+        elif existed.order < order:  # backward, -1
+            items = db.session.query(CITypeAttributeGroupItem).filter(
+                CITypeAttributeGroupItem.deleted.is_(False)).filter(
+                CITypeAttributeGroupItem.order > existed.order).filter(
+                CITypeAttributeGroupItem.order <= order)
+            for item in items:
+                item.update(order=item.order - 1)
+
+        existed.update(order=order)
+
+    @classmethod
+    def delete_item(cls, group_id, attr_id):
+        db.session.remove()
+
+        item = CITypeAttributeGroupItem.get_by(group_id=group_id,
+                                               attr_id=attr_id,
+                                               first=True,
+                                               to_dict=False)
+
+        if item is not None:
+            item.soft_delete()
+            order = item.order
+            gt_items = db.session.query(CITypeAttributeGroupItem).filter(
+                CITypeAttributeGroupItem.deleted.is_(False)).filter(CITypeAttributeGroupItem.order > order)
+            for _item in gt_items:
+                _order = _item.order
+                _item.update(order=_order - 1)
+
+    @classmethod
+    def transfer(cls, type_id, _from, _to):
+        current_app.logger.info("CIType[{0}] {1} -> {2}".format(type_id, _from, _to))
+        from_group = CITypeAttributeGroup.get_by_id(_from)
+        from_group or abort(404, "Group <{0}> is not found".format(_from))
+
+        to_group = CITypeAttributeGroup.get_by_id(_to)
+        to_group or abort(404, "Group <{0}> is not found".format(_to))
+
+        from_order, to_order = from_group.order, to_group.order
+
+        from_group.update(order=to_order)
+        to_group.update(order=from_order)
+
+        CITypeAttributesCache.clean(type_id)
+
+        ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE)
diff --git a/cmdb-api/api/lib/database.py b/cmdb-api/api/lib/database.py
index af89b6f..71a94fc 100644
--- a/cmdb-api/api/lib/database.py
+++ b/cmdb-api/api/lib/database.py
@@ -25,6 +25,9 @@ class FormatMixin(object):
 
 
 class CRUDMixin(FormatMixin):
+    def __init__(self, **kwargs):
+        super(CRUDMixin, self).__init__(**kwargs)
+
     @classmethod
     def create(cls, flush=False, **kwargs):
         return cls(**kwargs).save(flush=flush)
diff --git a/cmdb-api/api/lib/perm/auth.py b/cmdb-api/api/lib/perm/auth.py
index 22d82d8..5c5d25f 100644
--- a/cmdb-api/api/lib/perm/auth.py
+++ b/cmdb-api/api/lib/perm/auth.py
@@ -47,7 +47,7 @@ def _auth_with_token():
 
     try:
         token = auth_headers
-        data = jwt.decode(token, current_app.config['SECRET_KEY'])
+        data = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
         user = User.query.filter_by(email=data['sub']).first()
         if not user:
             return False
diff --git a/cmdb-api/api/models/cmdb.py b/cmdb-api/api/models/cmdb.py
index 31a22dc..0c0a953 100644
--- a/cmdb-api/api/models/cmdb.py
+++ b/cmdb-api/api/models/cmdb.py
@@ -15,13 +15,13 @@ from api.lib.database import Model
 class RelationType(Model):
     __tablename__ = "c_relation_types"
 
-    name = db.Column(db.String(16), index=True)
+    name = db.Column(db.String(16), index=True, nullable=False)
 
 
 class CITypeGroup(Model):
     __tablename__ = "c_ci_type_groups"
 
-    name = db.Column(db.String(32))
+    name = db.Column(db.String(32), nullable=False)
 
 
 class CITypeGroupItem(Model):
@@ -35,12 +35,12 @@ class CITypeGroupItem(Model):
 class CIType(Model):
     __tablename__ = "c_ci_types"
 
-    name = db.Column(db.String(32))
-    alias = db.Column(db.String(32))
+    name = db.Column(db.String(32), nullable=False)
+    alias = db.Column(db.String(32), nullable=False)
     unique_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False)
     enabled = db.Column(db.Boolean, default=True, nullable=False)
     is_attached = db.Column(db.Boolean, default=False, nullable=False)
-    icon_url = db.Column(db.String(256))
+    icon_url = db.Column(db.String(256), default='', nullable=False)
     order = db.Column(db.SmallInteger, default=0, nullable=False)
 
     unique_key = db.relationship("Attribute", backref="c_ci_types.unique_id")
@@ -89,7 +89,7 @@ class CITypeAttribute(Model):
 class CITypeAttributeGroup(Model):
     __tablename__ = "c_ci_type_attribute_groups"
 
-    name = db.Column(db.String(64))
+    name = db.Column(db.String(64), nullable=False)
     type_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
     order = db.Column(db.SmallInteger, default=0)
 
@@ -266,8 +266,8 @@ class OperationRecord(Model):
     __tablename__ = "c_records"
 
     uid = db.Column(db.Integer, index=True, nullable=False)
-    origin = db.Column(db.String(32))
-    ticket_id = db.Column(db.String(32))
+    origin = db.Column(db.String(32), nullable=False)
+    ticket_id = db.Column(db.String(32), nullable=False)
     reason = db.Column(db.Text)
 
 
diff --git a/cmdb-api/api/tasks/cmdb.py b/cmdb-api/api/tasks/cmdb.py
index 36853eb..7560b5e 100644
--- a/cmdb-api/api/tasks/cmdb.py
+++ b/cmdb-api/api/tasks/cmdb.py
@@ -11,6 +11,7 @@ from api.extensions import celery
 from api.extensions import db
 from api.extensions import es
 from api.extensions import rd
+from api.lib.cmdb.cache import CITypeAttributeCache
 from api.lib.cmdb.const import CMDB_QUEUE
 from api.lib.cmdb.const import REDIS_PREFIX_CI
 from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
@@ -71,3 +72,23 @@ def ci_relation_delete(parent_id, child_id):
     rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
 
     current_app.logger.info("DELETE ci relation cache: {0} -> {1}".format(parent_id, child_id))
+
+
+@celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE)
+def ci_type_attribute_order_rebuild(type_id):
+    current_app.logger.info('rebuild attribute order')
+    db.session.remove()
+
+    from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
+
+    attrs = CITypeAttributeCache.get(type_id)
+    id2attr = {attr.attr_id: attr for attr in attrs}
+
+    res = CITypeAttributeGroupManager.get_by_type_id(type_id, True)
+    order = 0
+    for group in res:
+        for _attr in group.get('attributes'):
+            if order != id2attr.get(_attr['id']) and id2attr.get(_attr['id']):
+                id2attr.get(_attr['id']).update(order=order)
+
+            order += 1
diff --git a/cmdb-api/api/views/cmdb/ci_type.py b/cmdb-api/api/views/cmdb/ci_type.py
index 554655d..50fc386 100644
--- a/cmdb-api/api/views/cmdb/ci_type.py
+++ b/cmdb-api/api/views/cmdb/ci_type.py
@@ -164,6 +164,34 @@ class CITypeAttributeView(APIView):
         return self.jsonify(attributes=attr_id_list)
 
 
+class CITypeAttributeTransferView(APIView):
+    url_prefix = "/ci_types/<int:type_id>/attributes/transfer"
+
+    @args_required('from')
+    @args_required('to')
+    def post(self, type_id):
+        _from = request.values.get('from')  # {'attr_id': xx, 'group_id': xx}
+        _to = request.values.get('to')  # {'group_id': xx, 'order': xxx}
+
+        CITypeAttributeManager.transfer(type_id, _from, _to)
+
+        return self.jsonify(code=200)
+
+
+class CITypeAttributeGroupTransferView(APIView):
+    url_prefix = "/ci_types/<int:type_id>/attribute_groups/transfer"
+
+    @args_required('from')
+    @args_required('to')
+    def post(self, type_id):
+        _from = request.values.get('from')  # group_id
+        _to = request.values.get('to')  # group_id
+
+        CITypeAttributeGroupManager.transfer(type_id, _from, _to)
+
+        return self.jsonify(code=200)
+
+
 class CITypeAttributeGroupView(APIView):
     url_prefix = ("/ci_types/<int:type_id>/attribute_groups",
                   "/ci_types/attribute_groups/<int:group_id>")
diff --git a/cmdb-api/tests/conftest.py b/cmdb-api/tests/conftest.py
index fee9d8f..d2f0c04 100644
--- a/cmdb-api/tests/conftest.py
+++ b/cmdb-api/tests/conftest.py
@@ -21,7 +21,7 @@ class CMDBTestClient(FlaskClient):
         headers.setdefault("User-Agent", "py.test")
         kwargs["headers"] = headers
 
-        json_data = kwargs.pop("json")
+        json_data = kwargs.pop("json", None)
         if json_data is not None:
             kwargs["data"] = json.dumps(json_data)
             if not kwargs.get("content_type"):
@@ -49,6 +49,7 @@ def app():
     _app.config['SECRET_KEY'] = CMDBTestClient.TEST_APP_SECRET
     _app.test_client_class = CMDBTestClient
     _app.response_class = CMDBTestResponse
+
     ctx = _app.test_request_context()
     ctx.push()
     yield _app
@@ -99,7 +100,6 @@ def clean_db():
         db.session.commit()
 
     if not User.get_by(email="test@xx.com"):
-        print("hello world xxxxx")
         u = User.create(
             flush=True,
             username="test",
diff --git a/cmdb-api/tests/sample.py b/cmdb-api/tests/sample.py
new file mode 100644
index 0000000..3bdc563
--- /dev/null
+++ b/cmdb-api/tests/sample.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+"""provide some sample data in database"""
+import uuid
+import random
+
+from api.models.cmdb import Attribute, CIType, CITypeAttributeGroup, CITypeAttribute
+
+
+def init_attributes(num=1):
+    attrs = []
+    for i in range(num):
+        attrs.append(Attribute.create(
+            name=uuid.uuid4().hex[:8],
+            alias=uuid.uuid4().hex[:8],
+            value_type=str(random.randint(0, 100) % 7)
+        ))
+    return attrs
+
+
+def init_ci_types(num=1):
+    attrs = init_attributes(num)
+
+    ci_types = []
+    for i in range(num):
+        ci_type = CIType.create(
+            name=uuid.uuid4().hex[:8],
+            alias=uuid.uuid4().hex[:8],
+            unique_id=attrs[i].id
+        )
+        CITypeAttribute.create(
+            type_id=ci_type.id,
+            attr_id=attrs[i].id,
+        )
+        ci_types.append(ci_type)
+
+    return ci_types
+
+
+def init_attribute_groups(num=1):
+    ci_types = init_ci_types(num)
+
+    ags = []
+    for i in range(num):
+        ags.append(CITypeAttributeGroup.create(
+            name=uuid.uuid4().hex[:8],
+            type_id=ci_types[i].id,
+            order=i
+        ))
+    return ags
diff --git a/cmdb-api/tests/test_cmdb_attribute.py b/cmdb-api/tests/test_cmdb_attribute.py
index fc754d9..eac93e0 100644
--- a/cmdb-api/tests/test_cmdb_attribute.py
+++ b/cmdb-api/tests/test_cmdb_attribute.py
@@ -1,6 +1,8 @@
 # -*- coding: utf-8 -*-
 from api.models.cmdb import Attribute
 
+from tests.sample import init_attributes
+
 
 def test_create_attribute(session, client):
     url = "/api/v0.1/attributes"
@@ -16,7 +18,7 @@ def test_create_attribute(session, client):
     assert resp.status_code == 200
     assert resp.json["attr_id"]
 
-    # check there is a ci_types in database
+    # check there is a attribute in database
     attr_id = resp.json["attr_id"]
     attr_ins = Attribute.get_by_id(attr_id)
     assert attr_ins.id == attr_id
@@ -24,3 +26,34 @@ def test_create_attribute(session, client):
     assert attr_ins.alias == "区域"
 
 
+def test_update_attribute(session, client):
+    attr_ins = init_attributes(1)[0]
+
+    url = "/api/v0.1/attributes/" + str(attr_ins.id)
+    payload = {
+        "name": "update",
+    }
+
+    resp = client.put(url, json=payload)
+
+    # check resp status code and content
+    assert resp.status_code == 200
+    assert resp.json["attr_id"] == attr_ins.id
+
+    # check attribute updated in database
+    attr_ins = Attribute.get_by_id(attr_ins.id)
+    assert attr_ins.name == "update"
+
+
+def test_delete_attribute(session, client):
+    attr_ins = init_attributes(1)[0]
+    url = "/api/v0.1/attributes/" + str(attr_ins.id)
+
+    resp = client.delete(url)
+
+    assert resp.status_code == 200
+    # attr should be soft delete
+    attr_ins = Attribute.get_by_id(attr_ins.id)
+    assert attr_ins.deleted is True
+    assert attr_ins.deleted_at
+
diff --git a/cmdb-api/tests/test_cmdb_ci.py b/cmdb-api/tests/test_cmdb_ci.py
index 78a93f1..faaaf79 100644
--- a/cmdb-api/tests/test_cmdb_ci.py
+++ b/cmdb-api/tests/test_cmdb_ci.py
@@ -1,10 +1,3 @@
 # -*- coding: utf-8 -*-
 
 
-class TestCI:
-
-    def test_ci_search_only_type_query(self, app):
-        with app.test_client() as c:
-            rv = c.get('/api/v0.1/ci/s?q=_type:server', json={})
-            json_data = rv.get_json()
-            assert type(json_data.get("result")) is list
diff --git a/cmdb-api/tests/test_cmdb_ci_type.py b/cmdb-api/tests/test_cmdb_ci_type.py
index 40a96af..fb7c23b 100644
--- a/cmdb-api/tests/test_cmdb_ci_type.py
+++ b/cmdb-api/tests/test_cmdb_ci_type.py
@@ -1 +1,167 @@
 # -*- coding: utf-8 -*-
+from api.models.cmdb import (
+    CIType, CITypeAttribute,
+    Attribute, CITypeAttributeGroup,
+    CITypeAttributeGroupItem)
+
+from tests.sample import (
+    init_attributes, init_ci_types,
+    init_attribute_groups)
+
+
+def test_create_ci_type(session, client):
+    attr = init_attributes(1)[0]
+
+    url = "/api/v0.1/ci_types"
+    payload = {
+        "name": "test",
+        "alias": "测试",
+        "unique_key": attr.id
+    }
+
+    resp = client.post(url, json=payload)
+
+    # check resp status code and content
+    assert resp.status_code == 200
+    assert resp.json["type_id"]
+
+    # check there is a attribute in database
+    type_id = resp.json["type_id"]
+    ci_type_ins = CIType.get_by_id(type_id)
+    assert ci_type_ins.id == type_id
+    assert ci_type_ins.name == "test"
+    assert ci_type_ins.alias == "测试"
+    assert ci_type_ins.unique_id == attr.id
+
+
+def test_update_ci_type(session, client):
+    ci_type_ins = init_ci_types(1)[0]
+
+    url = "/api/v0.1/ci_types/" + str(ci_type_ins.id)
+    payload = {
+        "name": "update",
+    }
+
+    resp = client.put(url, json=payload)
+
+    # check resp status code and content
+    assert resp.status_code == 200
+    assert resp.json["type_id"] == ci_type_ins.id
+
+    # check ci_type updated in database
+    ci_type_ins = CIType.get_by_id(ci_type_ins.id)
+    assert ci_type_ins.name == "update"
+
+
+def test_delete_ci_type(session, client):
+    ci_type_ins = init_ci_types(1)[0]
+    url = "/api/v0.1/ci_types/" + str(ci_type_ins.id)
+
+    resp = client.delete(url)
+
+    assert resp.status_code == 200
+    # attr should be soft delete
+    ci_type_ins = CIType.get_by_id(ci_type_ins.id)
+    assert ci_type_ins.deleted is True
+    assert ci_type_ins.deleted_at
+
+
+def test_bind_attributes_ci_type(session, client):
+    attrs = init_attributes(3)
+    ci_type = init_ci_types(1)[0]
+
+    url = "/api/v0.1/ci_types/{}/attributes".format(ci_type.id)
+    payload = {
+        "attr_id": [str(x.id) for x in attrs]
+    }
+
+    resp = client.post(url, json=payload)
+
+    # check resp status code and content
+    assert resp.status_code == 200
+    assert len(resp.json["attributes"]) == len(attrs)
+
+    # check ci_type has 4 attributes
+    ci_type_attribute_ids = [x.attr_id for x in CITypeAttribute.query.filter_by(type_id=ci_type.id).all()]
+    for attr in attrs:
+        assert attr.id in ci_type_attribute_ids
+
+
+def test_get_attributes_ci_type(session, client):
+    ci_type = init_ci_types(1)[0]
+    url = "/api/v0.1/ci_types/{}/attributes".format(ci_type.name)
+
+    resp = client.get(url)
+
+    assert resp.status_code == 200
+    assert len(resp.json["attributes"]) == 1
+
+
+def test_update_attributes_ci_type(session, client):
+    ci_type = init_ci_types(1)[0]
+    attr = Attribute.query.first()
+    url = "/api/v0.1/ci_types/{}/attributes".format(ci_type.id)
+
+    payload = {
+        "attributes": [
+            {"attr_id": attr.id, "default_show": False, "is_required": True}
+        ]
+    }
+    resp = client.put(url, json=payload)
+    assert resp.status_code == 200
+
+    ci_type_attr_ins = CITypeAttribute.query.filter_by(type_id=ci_type.id).first()
+    assert ci_type_attr_ins
+    assert ci_type_attr_ins.is_required is True
+    assert ci_type_attr_ins.default_show is False
+
+
+def test_create_attribute_group_ci_type(session, client):
+    ci_type = init_ci_types(1)[0]
+
+    url = "/api/v0.1/ci_types/{}/attribute_groups".format(ci_type.id)
+    payload = {
+        "name": "A",
+        "order": 100,
+    }
+
+    resp = client.post(url, json=payload)
+
+    # check resp status code and content
+    assert resp.status_code == 200
+    assert resp.json["group_id"]
+
+    ins = CITypeAttributeGroup.query.filter_by(type_id=ci_type.id).first()
+    assert ins
+    assert ins.id == resp.json["group_id"]
+    assert ins.name == "A"
+    assert ins.order == 100
+
+
+def test_update_attribute_group_ci_type(session, client):
+    attribute_groups = init_attribute_groups(1)[0]
+
+    url = "/api/v0.1/ci_types/attribute_groups/{}".format(attribute_groups.id)
+    payload = {
+        "attributes": [x.id for x in Attribute.query.all()]
+    }
+
+    resp = client.put(url, json=payload)
+    assert resp.status_code == 200
+    assert resp.json["group_id"]
+
+    ag_items = CITypeAttributeGroupItem.query.filter_by(group_id=attribute_groups.id).all()
+    for a in Attribute.query.all():
+        assert a.id in [x.attr_id for x in ag_items]
+
+
+def test_delete_attribute_group_ci_type(session, client):
+    attribute_groups = init_attribute_groups(1)[0]
+
+    url = "/api/v0.1/ci_types/attribute_groups/{}".format(attribute_groups.id)
+    resp = client.delete(url)
+
+    assert resp.status_code == 200
+    attribute_group_ins = CITypeAttributeGroup.query.filter_by(id=attribute_groups.id).first()
+    assert attribute_group_ins.deleted is True
+    assert attribute_group_ins.deleted_at
diff --git a/cmdb-ui/src/views/cmdb/ci/index.vue b/cmdb-ui/src/views/cmdb/ci/index.vue
index 62ceca3..b3cfde4 100644
--- a/cmdb-ui/src/views/cmdb/ci/index.vue
+++ b/cmdb-ui/src/views/cmdb/ci/index.vue
@@ -1,78 +1,126 @@
 <template>
-  <a-card :bordered="false">
-    <a-spin :tip="loadTip" :spinning="loading">
-      <search-form ref="search" @refresh="refreshTable" :preferenceAttrList="preferenceAttrList" />
+  <div>
+    <a-card :bordered="false">
+      <a-spin :tip="loadTip" :spinning="loading">
+        <search-form ref="search" @refresh="refreshTable" :preferenceAttrList="preferenceAttrList" />
 
-      <ci-detail ref="detail" :typeId="typeId" />
+        <ci-detail ref="detail" :typeId="typeId" />
 
-      <div class="table-operator">
-        <a-button
-          type="primary"
-          icon="plus"
-          @click="$refs.create.visible = true; $refs.create.action='create'"
-        >{{ $t('button.add') }}</a-button>
-        <a-dropdown v-action:edit v-if="selectedRowKeys.length > 0">
-          <a-menu slot="overlay">
-            <a-menu-item
-              key="batchUpdate"
-              @click="$refs.create.visible = true; $refs.create.action='update'"
-            >
-              <span @click="$refs.create.visible = true">
-                <a-icon type="edit" />&nbsp;修改
-              </span>
-            </a-menu-item>
-            <a-menu-item key="batchDownload" @click="batchDownload">
-              <json-excel :fetch="batchDownload" name="cmdb.xls">
-                <a-icon type="download" />&nbsp;下载
-              </json-excel>
-            </a-menu-item>
-            <a-menu-item key="batchDelete" @click="batchDelete">
-              <a-icon type="delete" />{{ $t('tip.delete') }}
-            </a-menu-item>
-          </a-menu>
-          <a-button style="margin-left: 8px">
-            {{ $t('ci.batchOperate') }}
-            <a-icon type="down" />
-          </a-button>
-        </a-dropdown>
-      </div>
-      <s-table
-        bordered
-        ref="table"
-        size="middle"
-        rowKey="ci_id"
-        :columns="columns"
-        :data="loadInstances"
-        :alert="options.alert"
-        :rowSelection="options.rowSelection"
-        :scroll="{ x: scrollX, y: scrollY }"
-        :pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
-        showPagination="auto"
-        :pageSize="25"
-      >
-        <template :slot="col.dataIndex" slot-scope="text, record" v-for="col in columns">
-          <editable-cell
-            :key="'edit_' + col.dataIndex"
-            :text="text"
-            @change="onCellChange(record.key, col.dataIndex, $event, record[col.dataIndex])"
-          />
-        </template>
-
-        <span slot="action" slot-scope="text, record">
-          <template>
-            <a
-              @click="$refs.detail.visible = true; $refs.detail.ciId = record.key; $refs.detail.create()"
-            >{{ $t('tip.detail') }}</a>
-
-            <a-divider type="vertical" />
-            <a @click="deleteCI(record)">{{ $t('tip.delete') }}</a>
+        <div class="table-operator">
+          <a-button
+            type="primary"
+            icon="plus"
+            @click="$refs.create.visible = true; $refs.create.action='create'"
+          >新建</a-button>
+          <a-button class="right" @click="showDrawer(typeId)">显示字段</a-button>
+          <a-dropdown v-action:edit v-if="selectedRowKeys.length > 0">
+            <a-menu slot="overlay">
+              <a-menu-item
+                key="batchUpdate"
+                @click="$refs.create.visible = true; $refs.create.action='update'"
+              >
+                <span @click="$refs.create.visible = true">
+                  <a-icon type="edit" />&nbsp;修改
+                </span>
+              </a-menu-item>
+              <a-menu-item key="batchDownload" @click="batchDownload">
+                <json-excel :fetch="batchDownload" name="cmdb.xls">
+                  <a-icon type="download" />&nbsp;下载
+                </json-excel>
+              </a-menu-item>
+              <a-menu-item key="batchDelete" @click="batchDelete">
+                <a-icon type="delete" />删除
+              </a-menu-item>
+            </a-menu>
+            <a-button style="margin-left: 8px">
+              批量操作
+              <a-icon type="down" />
+            </a-button>
+          </a-dropdown>
+        </div>
+        <s-table
+          bordered
+          ref="table"
+          size="middle"
+          rowKey="ci_id"
+          :columns="columns"
+          :data="loadInstances"
+          :alert="options.alert"
+          :rowSelection="options.rowSelection"
+          :scroll="{ x: scrollX, y: scrollY }"
+          :pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
+          showPagination="auto"
+          :pageSize="25"
+        >
+          <template :slot="col.dataIndex" slot-scope="text, record" v-for="col in columns">
+            <editable-cell
+              :key="'edit_' + col.dataIndex"
+              :text="text"
+              @change="onCellChange(record.key, col.dataIndex, $event, record[col.dataIndex])"
+            />
           </template>
-        </span>
-      </s-table>
 
-      <create-instance-form @refresh="refreshTable" ref="create" @submit="batchUpdate" />
-    </a-spin>
-  </a-card>
+          <span slot="action" slot-scope="text, record">
+            <template>
+              <a
+                @click="$refs.detail.visible = true; $refs.detail.ciId = record.key; $refs.detail.create()"
+              >详情</a>
+
+              <a-divider type="vertical" />
+              <a @click="deleteCI(record)">删除</a>
+            </template>
+          </span>
+        </s-table>
+
+        <create-instance-form @refresh="refreshTable" ref="create" @submit="batchUpdate" />
+      </a-spin>
+    </a-card>
+
+    <template>
+      <div>
+        <a-drawer
+          title="显示字段定义"
+          :width="600"
+          @close="onClose"
+          :visible="visible"
+          :wrapStyle="{height: 'calc(100% - 108px)', overflow: 'auto', paddingBottom: '108px'}"
+        >
+          <template>
+            <a-transfer
+              :dataSource="attrList"
+              :showSearch="true"
+              :listStyle="{
+                width: '230px',
+                height: '500px',
+              }"
+              :titles="['未选属性','已选属性']"
+              :render="item=>item.title"
+              :targetKeys="selectedAttrList"
+              @change="handleChange"
+              @search="handleSearch"
+            >
+              <span slot="notFoundContent">没数据</span>
+            </a-transfer>
+          </template>
+          <div
+            :style="{
+              position: 'absolute',
+              left: 0,
+              bottom: 0,
+              width: '100%',
+              borderTop: '1px solid #e9e9e9',
+              padding: '10px 16px',
+              background: '#fff',
+              textAlign: 'right',
+            }"
+          >
+            <a-button :style="{marginRight: '8px'}" @click="onClose">取消</a-button>
+            <a-button @click="subInstanceSubmit" type="primary">提交</a-button>
+          </div>
+        </a-drawer>
+      </div>
+    </template>
+  </div>
 </template>
 
 <script>
@@ -85,8 +133,9 @@ import CreateInstanceForm from './modules/CreateInstanceForm'
 import EditableCell from './modules/EditableCell'
 import CiDetail from './modules/CiDetail'
 import { searchCI, updateCI, deleteCI } from '@/api/cmdb/ci'
-import { getSubscribeAttributes } from '@/api/cmdb/preference'
+import { getSubscribeAttributes, subscribeCIType } from '@/api/cmdb/preference'
 import { notification } from 'ant-design-vue'
+import { getCITypeAttributesByName } from '@/api/cmdb/CITypeAttr'
 
 var valueTypeMap = {
   '0': 'int',
@@ -123,6 +172,10 @@ export default {
 
       preferenceAttrList: [],
 
+      selectedAttrList: [],
+      attrList: [],
+      visible: false,
+
       instanceList: [],
       // 表头
       columns: [],
@@ -202,6 +255,57 @@ export default {
   },
   inject: ['reload'],
   methods: {
+    showDrawer () {
+      this.getAttrList()
+    },
+    getAttrList () {
+      getCITypeAttributesByName(this.typeId).then(res => {
+        const attributes = res.attributes
+        getSubscribeAttributes(this.typeId).then(_res => {
+          const attrList = []
+          const selectedAttrList = []
+          const subAttributes = _res.attributes
+          this.instanceSubscribed = _res.is_subscribed
+          subAttributes.forEach(item => {
+            selectedAttrList.push(item.id.toString())
+          })
+
+          attributes.forEach(item => {
+            const data = {
+              key: item.id.toString(),
+              title: item.alias || item.name
+            }
+            attrList.push(data)
+          })
+
+          this.attrList = attrList
+          this.selectedAttrList = selectedAttrList
+          this.visible = true
+        })
+      })
+    },
+    onClose () {
+      this.visible = false
+    },
+    subInstanceSubmit () {
+      subscribeCIType(this.typeId, this.selectedAttrList)
+        .then(res => {
+          notification.success({
+            message: '修改成功'
+          })
+          this.reload()
+        })
+        .catch(e => {
+          notification.error({
+            message: e.response.data.message
+          })
+        })
+    },
+    handleChange (targetKeys, direction, moveKeys) {
+      this.selectedAttrList = targetKeys
+    },
+    handleSearch (dir, value) {},
+
     setColumnWidth () {
       let rows = []
       try {
@@ -445,15 +549,18 @@ export default {
 }
 </script>
 
-<style>
-.ant-table-thead > tr > th,
-.ant-table-tbody > tr > td {
+<style lang='less' scoped>
+/deep/ .ant-table-thead > tr > th,
+/deep/ .ant-table-tbody > tr > td {
   white-space: nowrap;
   overflow: hidden;
 }
-.spin-content {
+/deep/ .spin-content {
   border: 1px solid #91d5ff;
   background-color: #e6f7ff;
   padding: 30px;
 }
+.right {
+  float: right;
+}
 </style>