--- id: udk-accounts-list title: Implementing the accounts component pagination_label: UDK sidebar_label: Accounts Component sidebar_position: 2 sidebar_class_name: rudk keywords: ['UI', 'development', 'kit'] description: Develop custom user interfaces. slug: /tools/ui-development-kit/accounts-list tags: ['UI'] --- ## Overview This guide will walk you through the process of implementing a new component that lists all the accounts in your tenant. This serves as an example you can use to learn how to implement many different types of custom UI pages. This guide is a continuation of [Getting Started](./getting-started). At the end of this guide, you will be able to do the following: - List all accounts in your tenant - Paginate accounts by page limits of 10, 25, 50, 100 - Click through each page of results - View the JSON response of the account from your tenant - Sort the results, using the sort syntax on the [List Accounts endpoint](https://developer.sailpoint.com/docs/api/v3/list-accounts). - Filter the results, using the filter syntax on the [List Accounts endpoint](https://developer.sailpoint.com/docs/api/v3/list-accounts). ## Create the accounts component Create the accounts component by running the following command in the root of the project: ```bash npm run generate:component accounts ``` This command will create a new folder in the `projects/sailpoint-components/src/lib` directory with the name `accounts`. It will also create the following files: - `accounts.component.html` - `accounts.component.ts` - `accounts.component.scss` - `accounts.component.spec.ts` The output will look similar to this: ```bash tyler.mairose ~/development/ui-development-kit [main] $ npm run generate:component accounts > sailpoint-ui-development-kit@1.0.0 generate:component > node scripts/generate-component.js accounts 🚀 Generating component: accounts ✅ Created directory: /Users/tyler.mairose/development/ui-development-kit/projects/sailpoint-components/src/lib/accounts ✅ Created: accounts.component.ts ✅ Created: accounts.component.html ✅ Created: accounts.component.scss ✅ Created: accounts.component.spec.ts ✅ Updated: component-selector.service.ts ✅ Updated: app.routes.ts ✅ Updated: app.component.html ✅ Updated: public-api.ts 🎉 Component 'accounts' generated successfully! 📝 Next steps: 1. Build the project: npm run start 2. Enable the component in the component selector 3. Implement your component logic in: /Users/tyler.mairose/development/ui-development-kit/projects/sailpoint-components/src/lib/accounts ``` ## Enable the accounts component To enable the accounts component, open the component selector and toggle the accounts component on. You can do this by clicking on the "Component Selector" tab in the sidebar. ![enable account component](./img/enable-accounts.png) You will see the accounts component in the list of available components. Toggle the accounts component on to enable it. Once enabled you will see the accounts component in the sidebar. You can click on it to navigate to the accounts page. ![accounts page](./img/accounts-component.png) ## Get account data The first thing your new component needs to do is get the account data to show. You can implement this by using the [List Accounts endpoint](https://developer.sailpoint.com/docs/api/v2025/list-accounts). To do so, add this logic to the typescript file at `projects/sailpoint-components/src/lib/accounts/accounts.component.ts`: ```typescript import { CommonModule } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatTableModule } from '@angular/material/table'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatPaginatorModule } from '@angular/material/paginator'; import { SailPointSDKService } from '../sailpoint-sdk.service'; import { AccountV2025 } from 'sailpoint-api-client'; @Component({ selector: 'app-accounts', standalone: true, imports: [ CommonModule, MatButtonModule, MatCardModule, MatIconModule, MatTableModule, MatToolbarModule, MatProgressSpinnerModule, MatPaginatorModule ], templateUrl: './accounts.component.html', styleUrl: './accounts.component.scss', }) export class AccountsComponent implements OnInit { title = 'Accounts'; loading = true; accounts: AccountV2025[] = []; displayedColumns: string[] = ['id', 'name', 'nativeIdentity', 'sourceId', 'disabled', 'locked', 'actions']; constructor(private sdk: SailPointSDKService) {} ngOnInit() { // Load initial data void this.loadAccounts(); } private async loadAccounts() { this.loading = true; try { const response = await this.sdk.listAccounts(); this.accounts = response.data as AccountV2025[]; console.log('Loaded accounts:', this.accounts); } catch (error) { console.error('Error loading accounts:', error); } finally { this.loading = false; } } viewAccount(account: AccountV2025): void { console.log('Viewing account:', account); } } ``` Return to your accounts list page. In the electron app, click View -> Toggle Developer Tools. You will see the response containing the accounts in the console after the page loads. Now that you have your account data, you need to display the data. You can add a table to the UI and display your results. To do so, add this code to `\accounts\accounts.component.html`:
Show code ```html
account_box {{ title }}
ID {{ account.id }} Name {{ account.name || '-' }} Native Identity {{ account.nativeIdentity || '-' }} Source {{ account.sourceId || '-' }} Disabled check_circle cancel Locked lock lock_open Actions
No accounts found.
```
Save the `\accounts\accounts.component.html` file and return to the accounts list page. You will see up to 250 accounts in the table. ## Pagination You will likely have more than 250 accounts in your tenant. To handle more than 250 accounts, you must implement pagination on your front end page. Pagination is the process of paging through your records. With pagination, you can handle 1000 accounts in 4 pages of 250, for example. To implement pagination, add the following code. This code allows you to paginate accounts in groups of 5, 10, 50, 100 and 250, depending on your requirement: The highlighted portions of the code are the changes made to the previous code to allow pagination. import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs'; ```html
account_box {{ title }}
ID {{ account.id }} Name {{ account.name || '-' }} Native Identity {{ account.nativeIdentity || '-' }} Source {{ account.sourceId || '-' }} Disabled check_circle cancel Locked lock lock_open Actions
// highlight-start // highlight-end
No accounts found.
```
```typescript import { CommonModule } from '@angular/common'; // highlight-next-line import { Component, OnInit, ViewChild } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatTableModule } from '@angular/material/table'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; // highlight-next-line import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator'; import { SailPointSDKService } from '../sailpoint-sdk.service'; import { AccountV2025 } from 'sailpoint-api-client'; @Component({ selector: 'app-accounts', standalone: true, imports: [ CommonModule, MatButtonModule, MatCardModule, MatIconModule, MatTableModule, MatToolbarModule, MatProgressSpinnerModule, MatPaginatorModule ], templateUrl: './accounts.component.html', styleUrl: './accounts.component.scss', }) export class AccountsComponent implements OnInit { title = 'Accounts'; loading = true; accounts: AccountV2025[] = []; displayedColumns: string[] = ['id', 'name', 'nativeIdentity', 'sourceId', 'disabled', 'locked', 'actions']; // highlight-start // Pagination settings pageSize = 10; pageIndex = 0; totalCount = 0; @ViewChild(MatPaginator) paginator!: MatPaginator; // highlight-end constructor(private sdk: SailPointSDKService) {} ngOnInit() { // Load initial data void this.loadAccounts(); } async loadAccounts() { // highlight-start // Setup request for paged account results const request = { offset: this.pageIndex * this.pageSize, limit: this.pageSize, count: true, sorters: undefined, filters: undefined }; this.loading = true; try { const response = await this.sdk.listAccounts(request); this.accounts = response.data; // Get total count from headers if available let count: number | undefined; if (response.headers && typeof (response.headers as any).get === 'function') { const headerValue = (response.headers as any).get('X-Total-Count'); count = headerValue ? Number(headerValue) : undefined; } else if (response.headers && typeof (response.headers as any)['x-total-count'] !== 'undefined') { count = Number((response.headers as any)['x-total-count']); } this.totalCount = count ?? 250; // Default to 250 if count not available console.log('Loaded accounts:', this.accounts); // highlight-end } catch (error) { console.error('Error loading accounts:', error); } finally { this.loading = false; } } // highlight-start // Handle page change events onPageChange(event: PageEvent) { this.pageSize = event.pageSize; this.pageIndex = event.pageIndex; void this.loadAccounts(); } // highlight-end viewAccount(account: AccountV2025): void { console.log('Viewing account:', account); } } ```
Return to the accounts list page. You will see the paginator at the bottom of the page. You can now paginate through the accounts in your tenant. ## Viewing Details In this step, we will create a detail view to see the raw json object that represents the underlying data. To do this, we will implement the already existing `viewAccount` method to use the `GenericDialogComponent` that comes with the UI Development Kit. The only thing to change here is just to implement the method as seen below: ```typescript viewAccount(account: AccountV2025): void { // Format account details as JSON string with indentation const details = JSON.stringify(account, null, 2); // Open dialog with account details this.dialog.open(GenericDialogComponent, { minWidth: '800px', data: { title: `Account Details: ${account.name || account.nativeIdentity || account.id}`, message: details } }); } ``` Note that we also need to add the dialog to the constructor: ```typescript constructor(private sdk: SailPointSDKService, private dialog: MatDialog) {} ``` And also add the imports: ```typescript import { MatDialog } from '@angular/material/dialog'; import { GenericDialogComponent } from '../generic-dialog/generic-dialog.component'; ``` ## Sort and filter The last part of the page we may want to implement would be sorting and filtering. With this implementation, the `this.filterForm.valueChanges` event and `onSortChange` event will cause the page to reload the accounts with the new sort or filter applied. The completed component and all code can be seen below: ```html
account_box {{ title }}
Name Source ID Correlated {{option.label}}
ID {{ account.id }} Name {{ account.name || '-' }} Native Identity {{ account.nativeIdentity || '-' }} Source {{ account.sourceId || '-' }} Disabled check_circle cancel Locked lock lock_open Actions
No accounts found.
```
```typescript import { CommonModule } from '@angular/common'; import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSort, MatSortModule, Sort, SortDirection } from '@angular/material/sort'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatDialog } from '@angular/material/dialog'; import { GenericDialogComponent } from '../generic-dialog/generic-dialog.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatTableModule } from '@angular/material/table'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator'; import { SailPointSDKService } from '../sailpoint-sdk.service'; import { AccountV2025 } from 'sailpoint-api-client'; @Component({ selector: 'app-accounts', standalone: true, imports: [ CommonModule, MatButtonModule, MatCardModule, MatIconModule, MatTableModule, MatToolbarModule, MatProgressSpinnerModule, MatPaginatorModule, MatSortModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, GenericDialogComponent ], templateUrl: './accounts.component.html', styleUrl: './accounts.component.scss', }) export class AccountsComponent implements OnInit { title = 'Accounts'; loading = true; accounts: AccountV2025[] = []; error = false; errorMessage = ''; displayedColumns: string[] = ['id', 'name', 'nativeIdentity', 'sourceId', 'disabled', 'locked', 'actions']; // Sort settings sortActive = 'name'; sortDirection: SortDirection = 'asc'; // Filter form filterForm = new FormGroup({ name: new FormControl(''), sourceId: new FormControl(''), correlated: new FormControl('') }); // Filter options correlatedOptions = [ { value: '', label: 'All' }, { value: 'true', label: 'Correlated' }, { value: 'false', label: 'Uncorrelated' } ]; // Pagination settings pageSize = 10; pageIndex = 0; totalCount = 0; @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort!: MatSort; constructor(private sdk: SailPointSDKService, private dialog: MatDialog) {} ngOnInit() { // Load initial data void this.loadAccounts(); // Subscribe to filter changes this.filterForm.valueChanges.subscribe(() => { this.pageIndex = 0; // Reset to first page on filter change void this.loadAccounts(); }); } async loadAccounts() { // Setup request for paged account results const request = { offset: this.pageIndex * this.pageSize, limit: this.pageSize, count: true, sorters: this.buildSorters(), filters: this.buildFilters() }; this.loading = true; this.error = false; this.errorMessage = ''; try { const response = await this.sdk.listAccounts(request); if (response.status !== 200) { throw new Error(`Failed to load accounts: ${response.statusText}`); } this.accounts = response.data; // Get total count from headers if available let count: number | undefined; if (response.headers && typeof (response.headers as any).get === 'function') { const headerValue = (response.headers as any).get('X-Total-Count'); count = headerValue ? Number(headerValue) : undefined; } else if (response.headers && typeof (response.headers as any)['x-total-count'] !== 'undefined') { count = Number((response.headers as any)['x-total-count']); } this.totalCount = count ?? 250; // Default to 250 if count not available } catch (error) { console.error('Error loading accounts:', error); this.error = true; this.errorMessage = error instanceof Error ? error.message : String(error); this.accounts = []; } finally { this.loading = false; } } // Handle page change events onPageChange(event: PageEvent) { this.pageSize = event.pageSize; this.pageIndex = event.pageIndex; void this.loadAccounts(); } // Handle sort changes onSortChange(event: Sort) { this.sortActive = event.active; this.sortDirection = event.direction as SortDirection; void this.loadAccounts(); } // Reset filters resetFilters() { this.filterForm.reset({ name: '', sourceId: '', correlated: '' }); } // Build sorters string for API request buildSorters(): string | undefined { if (!this.sortActive || this.sortDirection === '') { return undefined; } // For descending order, prefix column name with minus sign return this.sortDirection === 'desc' ? `-${this.sortActive}` : this.sortActive; } // Build filters string for API request buildFilters(): string | undefined { const filters: string[] = []; const formValues = this.filterForm.value; if (formValues.name) { filters.push(`name sw "${formValues.name}"`); } if (formValues.sourceId) { filters.push(`sourceId eq "${formValues.sourceId}"`); } if (formValues.correlated) { filters.push(`identity.correlated eq ${formValues.correlated}`); } return filters.length > 0 ? filters.join(' and ') : undefined; } viewAccount(account: AccountV2025): void { // Format account details as JSON string with indentation const details = JSON.stringify(account, null, 2); // Open dialog with account details this.dialog.open(GenericDialogComponent, { minWidth: '800px', data: { title: `Account Details: ${account.name || account.nativeIdentity || account.id}`, message: details } }); } } ``` ```css .accounts-container { height: 100%; display: flex; flex-direction: column; } .table-container { display: flex; flex-direction: column; } .toolbar-title { margin-left: 16px; } .content { padding: 24px; flex: 1; overflow-y: auto; } .filter-panel { margin-bottom: 20px; padding: 16px; border-radius: 4px; background-color: #f8f8f8; } .filter-row { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; } .filter-field { flex: 1; min-width: 200px; } :host-context(.dark-theme) .filter-panel { background-color: #333; } mat-card { max-width: 800px; margin: 0 auto; } mat-card-actions { display: flex; gap: 8px; padding: 16px; } mat-card-actions button { display: flex; align-items: center; gap: 8px; } ::ng-deep mat-spinner circle { stroke: #0033a1; /* teal-like custom color */ } .spinner-container { display: flex; justify-content: center; // horizontal centering align-items: center; // vertical centering border: none; height: 75vh; // takes full viewport height (adjust as needed) } .header-content { display: flex; justify-content: space-between; align-items: center; } .sortable { cursor: pointer; } .sortable:hover { background-color: #f3f3f3; } .sort-icon { margin-left: 6px; font-size: 0.95rem; color: black; // Highlight active sort &.active { color: #415364; font-weight: bold; } } td.mat-cell, th.mat-header-cell { vertical-align: middle; } td.mat-cell:last-child, th.mat-header-cell:last-child { text-align: center; } #viewIdentity, #attibuteDetails, #managerDetails { padding: 8px; margin-bottom: 10px; margin-top: 10px; width: 55px; } #attibuteDetails, #managerDetails { width: 125px; } :host { /* Dark mode overrides */ :host-context(.dark-theme) .sortable:hover { background-color: #2c2c2c; /* Darker hover background */ } :host-context(.dark-theme) .sort-icon { margin-left: 6px; font-size: 0.95rem; color: #ffffff; // Highlight active sort &.active { color: #ffffff; font-weight: bold; } } } ```
## Error handling You have now implemented a new page that lists all the accounts in your tenant, and you can now paginate, sort and filter the accounts in your tenant. Ideally, everything in your custom UIs will work smoothly, but you will likely encounter errors at some point when you're implementing a page. For example, if you provide an invalid filter or sorter, the list accounts endpoint will return a 400 error. You can see that there is a `try catch` block on the loadAccounts method that currently shows a console.log error but a custom message could be implemented to notify the user of a problem. This is not covered in this part of the tutorial, but with angular, presenting the user about an error is quite trivial. To learn more about handling errors in your UI, refer to [Error Handling](./error-handling). ## Discuss The most valuable resource for ISC developers is the SailPoint Developer Community itself, where ISC users and experts all over the world come together to ask questions and provide solutions. To learn more about the ISC UI Development Kit and discuss it with SailPoint Developer Community members, go to the [SailPoint Developer Community Forum](https://developer.sailpoint.com/discuss/c/identity-security-cloud/6).