Q2 Console Extension Tutorial (beta)

Warning

This is an early build of Console in the sandbox. There’s bound to be some weirdness. We look forward to hearing about it through our ticketing system. Some things that we know about:

  • To log out, you have to open your dev tools and clear your local storage variables. (In chrome that’s Dev Tools -> Application -> Local storage -> https://stack.q2developer.com -> Clear All)

  • Only the ‘sdkteam’ stack works right now we’re working on adding more support. If you want your stack as well, we will have to set it up manually for the moment.

  • This tutorial kind stinks. While the code samples work, there’s not yet any of the “now let’s move to step 2” language. Figured it’s better to release this big pile of code as is than wait for perfection.

  • The installer can’t tell the difference between Central and Console. Mostly this is fine, but it leads to some strange interactions (like always choose “Services” when installing in the nav)

Q2 Console is a web based backoffice tool and a spiritual successor to Q2 Central’s Windows based application. Rather than a different instance for each financial institution stack, there is just a single portal with a authentication layer that determines which stacks are presented.

For developing against our sandboxes, we have a q2developer hosted Q2Console. When the time comes to deploy your extension to a higher environment (staging/PTE/production) you will work with Q2’s standard support team to ensure you have login access to the “real” Q2Console.

In this tutorial, we will extend the Q2Console interface using the Q2Console extension type.

As always, let’s start by creating a new extension:

(.env) $ q2 create_extension ViewUserData

What type of extension are you creating?

    1) Online (default)
    2) SSO (Third Party Integration)
    3) Ardent (API)
    4) Q2Console (Backoffice)  <---------------------------
    5) Central (Legacy Backoffice)
    6) Adapter
    7) Audit Action
    8) Custom Health Check
    9) Message Bus
    10) Caliper API Custom Endpoint
    11) Base Extension

Please make a selection and press Return [1]: 4
Q2Console (Backoffice)
Select Q2Console type to generate

    1) Server Side Rendered
    2) Client Side Rendered  <---------------------------

Please make a selection and press Return: 2
Client Side Rendered

What type of framework would you like to integrate with?

    1) Continue with no framework  <---------------------------
    2) Integrate with React
    3) Integrate with Vue

Please make a selection and press Return [1]:
Preferred HMR port? [random]:

Most of our tutorials use Server Side Rendered extensions. These work with Q2Console types, too. But to mix it up, let’s make this look really nice with a Client Side Rendered extension. This tutorial is self contained, but for a deeper dive on CSR capabilities, feel free to check out Client Side Rendered Extensions. In addition, while it’s possible to use a framework such as React, Vue, Angular, etc for this, Tecton gives us a lot of wonderful capabilities out of the box with regular vanilla javascript. Let’s see how far we can get without using a framework at all!

TODO:
  • q2 install
    • Choose defaults

    • Add to services nav

Lots of files to make a full featured example:

These both live in the ViewUserData directory

"""
ViewUserData Extension
"""
from q2_sdk.core.http_handlers.console_handler import Q2ConsoleClientRequestHandler
from q2_sdk.hq.hq_api.backoffice import GetDataSetFiltered

from .models import MappedGroup


class ViewUserDataHandler(Q2ConsoleClientRequestHandler):
    CONFIG_FILE_NAME = "ViewUserData"  # configuration/ViewUserData.py file must exist if REQUIRED_CONFIGURATIONS exist

    @property
    def router(self):
        router = super().router
        router.update(
            {
                "default": self.default,
                "get_customers": self.get_customers,
                "get_users": self.get_users,
                "get_groups": self.get_groups,
                "get_user_data": self.get_user_data,
                "update_user_data": self.update_user_data,
                "create_user_data": self.create_user_data,
            }
        )

        return router

    async def default(self):
        return {"message": "Beautiful User Picker"}

    async def get_groups(self):
        cached_mapped_groups: dict = self.cache.get("mapped_groups", {"groups": []})
        mapped_group_list = []
        if cached_mapped_groups["groups"]:
            mapped_group_list = [
                MappedGroup.from_dict(group) for group in cached_mapped_groups["groups"]
            ]
        else:
            groups = await self.db.group.get()
            mapped_group_list = [
                MappedGroup(
                    group.GroupID.pyval,
                    group.GroupDesc.text,
                    group.IsTreasury.pyval,
                    group.IsCommercial.pyval,
                )
                for group in groups
            ]
            self.cache.set(
                "mapped_groups",
                {"groups": [group.to_dict() for group in mapped_group_list]},
                expire=60 * 10,
            )

        return {
            "rows": [
                {
                    "id": x.group_id,
                    "groupId": x.group_id,
                    "name": x.group_desc,
                    "isTreasury": x.is_treasury,
                    "isCommercial": x.is_commercial,
                }
                for x in mapped_group_list
            ],
            "total_size": len(mapped_group_list),
        }

    async def get_customers(self):
        search_id = self.form_fields['search_id']
        params_obj = GetDataSetFiltered.ParamsObj(
            self.logger,
            self.hq_credentials,
            GetDataSetFiltered.dataSetType.CustomerList,
            1,
            1000,
            filter_items=GetDataSetFiltered.filterItems(
                [
                    GetDataSetFiltered.FilterItem(
                        False,
                        GetDataSetFiltered.Operand.EQ,
                        0,
                        0,
                        field_name='groupID',
                        comparison_value=search_id,
                    )
                ]
            ),
        )
        result = await GetDataSetFiltered.execute(params_obj)
        customers = result.result_node.find('.//Q2_CustomerList')
        return {
            "rows": [
                {
                    "id": x.CustomerID.pyval,
                    "customerID": x.CustomerID.pyval,
                    "name": x.CustomerName.pyval,
                    "userCount": x.ActiveUserCount.pyval,
                    "groupId": x.GroupID.pyval,
                }
                for x in customers
            ],
            "total_size": len(customers),
        }

    async def get_users(self):
        search_id = self.form_fields['search_id']
        params_obj = GetDataSetFiltered.ParamsObj(
            self.logger,
            self.hq_credentials,
            GetDataSetFiltered.dataSetType.UserList,
            1,
            1000,
            filter_items=GetDataSetFiltered.filterItems(
                [
                    GetDataSetFiltered.FilterItem(
                        False,
                        GetDataSetFiltered.Operand.EQ,
                        0,
                        0,
                        field_name='customerID',
                        comparison_value=search_id,
                    )
                ]
            ),
        )
        result = await GetDataSetFiltered.execute(params_obj)
        users = result.result_node.find('.//Q2_UserList')
        return {
            "rows": [
                {
                    "id": x.UserID.pyval,
                    "userID": x.UserID.pyval,
                    "firstName": x.FirstName.pyval,
                    "lastName": x.LastName.pyval,
                    "customerID": x.CustomerID.pyval,
                }
                for x in users
            ],
            "total_size": len(users),
        }

    async def get_user_data(self):
        search_id = self.form_fields['search_id']
        user_data = await self.db.user_data.get_multi(search_id)
        return {
            "rows": [
                {
                    "id": x.DataID.pyval,
                    "userId": x.UserID,
                    "key": x.ShortName,
                    "value": x.GTDataValue,
                }
                for x in user_data
            ],
            "total_size": 1,
        }

    async def update_user_data(self):
        user_id = self.form_fields['userId']
        userdata_key = self.form_fields['key']
        userdata_value = self.form_fields['value']
        await self.db.user_data.update(user_id, userdata_key, userdata_value)
        self.logger.info('Updated UserData "%s" for UserId %d', userdata_key, user_id)
        return {'status': 'success'}


    async def create_user_data(self):
        user_id = self.form_fields['userId']
        userdata_key = self.form_fields['key']
        userdata_value = self.form_fields['value']
        await self.db.user_data.create(user_id, userdata_key, userdata_value)
        self.logger.info('Added UserData "%s" for UserId %d', userdata_key, user_id)
        return {'status': 'success'}