Disappearing custom navigation service after login

On Cumulocity v.10.15, we developed a custom Navigator Service, to replace the Cockpit’s default left Nav-column. This is mainly to have a nav-column with filtered items, expanded groups and customized groups icons. The column is perfectly visible, except after login, only the “Home” tab is visible and the rest of the column disappears. After refreshing, the column would be visible once again.

We are providing a CustomNavigationService in the NgModule, which fetches all assets groups and filter all items, then adds filtered items and groups as items. Groups are added with all properties, and functionality works well.

No errors are thrown at both scenarios. Trying to delay the adding of all items to the navigator items until groups are fetched didn’t fix the issue, and still only the “Home” tab would appear alone.

Nav-column after login:

nav-column-after-login

Nav-column after refresh:

nav-column-after-refresh

Custom Navigator Service code:

@Injectable()
export class CustomNavigationService extends NavigatorService {
    /**
     * contains all of the downloaded groups
     */
    groups: {managedObjects: any[]};

    constructor(rootInjector: Injector, router: Router, private fetchClient: FetchClient) {
        super(rootInjector, router);
        this.fetchGroups();
        //setTimeout(() => {
            this.items$ = this.items$.pipe(map((items) => {

                // filters unwanted menu items
                let filteredItems = items.filter(item =>
                  item.label !== 'Alarms'
                  && item.label !== 'Groups'
                  && item.label !== 'Reports'
                  && item.label !== 'Data explorer'
                  && item.label !== 'Configuration');

                if (isArray(this.groups?.managedObjects)) {
                    // adds new items to the menu based on the downloaded groups
                    filteredItems.push(...this.groups?.managedObjects?.map(group => {
                        return {
                            label: group.name,
                            path: 'group/' + group.id,
                            icon: 'group',
                            children: group.childAssets.references.map(device => { // creates children menu items based on the gruops devices
                                return {
                                    label: device.managedObject.name,
                                    path: 'device/' + device.managedObject.id,
                                    open: false,
                                    routerLinkExact: false,
                                    children: [],
                                    destroy() {

                                    },
                                    openOnStart(url: string): boolean {
                                        return false;
                                    },
                                    click() {
                                        router.navigate(['device', device.managedObject.id])
                                    },
                                    find() {

                                    }
                                }
                            }),
                            open: true,
                            routerLinkExact: false,
                            openOnStart(url: string): boolean {
                                return true;
                            },
                            click(options?: ClickOptions) {
                                // this.open = true;
                                router.navigate(['group', group.id]);
                            },
                            find(predicate: any, findBy?: keyof Pick<NavigatorNode, "label" | "featureId">): any {

                            }
                        } as AssetNode
                    }));
                }

                return filteredItems;
            }));
        //}, 5000);
    }

    /**
     * downloads all of the gruops from the backend and puts them into the 'groups' field
     */
    async fetchGroups() {
        this.groups = await (await this.fetchClient.fetch('/inventory/managedObjects/', {
            params: {
                pageSize: 2000,
                withTotalPages: true,
                query: 'has(c8y_IsDeviceGroup)'
            }
        })).json();
    };
}

Hi Omar,

we could provide you better support if you would provide your custom Navigator Service so we could look into it.

Hi Tristan. I’ve added the custom Navigator Service code to the topic

Hi Omar,

at the point of time where you are calling fetchGroups in the constructor, the user has not yet logged in (you should also see a failing request in your browsers developer tools).
If you would use the usual approach for adding further NavigatorNodes via the dedicated hook, this should not happen.

If you want to keep on adding your own nodes via your custom service, you just need to wait for the user to login before calling fetchGroups.

2 Likes

Hi Tristan. I would like to use the NavigatorNodes approach you mentioned. I’m trying to fetch all device groups and create a node for each and sub node for each subgroup/device. Trying to use HOOK_NAVIGATOR_NODES allows me to add a single node only. Documentation here isn’t very clear how to implement such case. Even trying to write a service for the HOOK_NAVIGATOR_NODES’s get() method and trying to return a list of nodes instead of a single node didn’t work. Could you propose a way to hook a navigatorNode per group/device using the provided hooks?

Hi Omar,

what you want to implement is the NavigatorNodeFactory interface which is nothing different as an ExtensionFactory<NavigatorNode> as documented here.
The get method needs to return one of these types: NavigatorNode | NavigatorNode[] | Observable<NavigatorNode | NavigatorNode[]> | Promise<NavigatorNode | NavigatorNode[]>.

An implementation of this, could look like this:

import { Injectable } from '@angular/core';
import { IManagedObject, InventoryService } from '@c8y/client';
import { NavigatorNode, NavigatorNodeFactory } from '@c8y/ngx-components';

@Injectable({ providedIn: 'root' })
export class CustomNavigatorNodes implements NavigatorNodeFactory {
  private nodes: NavigatorNode[];

  constructor(private inventory: InventoryService) {}

  async get(): Promise<NavigatorNode | NavigatorNode[]> {
    if (this.nodes) {
      return this.nodes;
    }
    const groups = await this.getGroups();
    this.nodes = groups.map(group => {
      const groupNode = new NavigatorNode({
        label: group.name,
        path: 'group/' + group.id,
        icon: 'group',
        routerLinkExact: false,
        open: true
      });

      const children = group.childAssets.references.map(device => {
        return new NavigatorNode({
          label: device.managedObject.name,
          path: 'device/' + device.managedObject.id,
          open: false,
          routerLinkExact: false
        });
      });

      children.forEach(child => {
        groupNode.add(child);
      });

      return groupNode;
    });
    return this.nodes;
  }

  async getGroups(): Promise<IManagedObject[]> {
    const { data: groups } = await this.inventory.list({
      pageSize: 2000,
      withTotalPages: true,
      query: 'has(c8y_IsDeviceGroup)'
    });
    return groups;
  }
}

An even nicer way could be to utilize the AssetNodeService for this:

import { Injectable } from '@angular/core';
import { IManagedObject, InventoryService } from '@c8y/client';
import { NavigatorNode, NavigatorNodeFactory } from '@c8y/ngx-components';
import { AssetNodeService } from '@c8y/ngx-components/assets-navigator';

@Injectable({ providedIn: 'root' })
export class CustomNavigatorNodes implements NavigatorNodeFactory {
  private nodes: NavigatorNode[];

  constructor(private inventory: InventoryService, private assetNodeService: AssetNodeService) {}

  async get(): Promise<NavigatorNode | NavigatorNode[]> {
    if (this.nodes) {
      return this.nodes;
    }
    const groups = await this.getGroups();
    this.nodes = groups.map(group => {
      const groupNode = this.assetNodeService.createChildNode(group, {});
      return groupNode;
    });
    return this.nodes;
  }

  async getGroups(): Promise<IManagedObject[]> {
    const { data: groups } = await this.inventory.list({
      pageSize: 2000,
      withChildren: false,
      query: 'has(c8y_IsDeviceGroup)'
    });
    return groups;
  }
}

You can provide these services in your module in newer versions (1017+) like this:

providers: [hookNavigator(CustomNavigatorNodes)]

in older versions it would look like this:

providers: [{ provide: HOOK_NAVIGATOR_NODES, useClass: CustomNavigatorNodes, multi: true }]

Regards,
Tristan

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.