feat(ui): add notification pane (click bell to access)
feat(ui): add REST service calls for notifications
feat(ui): add interfaces for many Apiman Manager API types
Can click through to the appropriate page for each notification/event received.
ADD
manager/ui/war/entry.ts
+// Browserify Entry Point
+// This file includes a list of dependencies we want to use on the client, but must be
+// installed server-side with npm. Browserify will check all requires(), subsequently
+// bundling them into a single file, which we can then use on the client.
+
+import * as $ from 'jquery';
+// @ts-ignore
+window.jQuery = $;
+// @ts-ignore
+window.$ = $;
+import 'bootstrap/dist/js/bootstrap.js';
+import 'bootstrap-select/dist/js/bootstrap-select.js';
+import 'json-editor';
+import 'patternfly/dist/js/patternfly.js';
+import 'angular';
+import 'angular-ui-bootstrap/ui-bootstrap.js';
+import 'angular-ui-bootstrap/ui-bootstrap-tpls.js';
+import 'ui-select/dist/select.js';
+import 'angular-clipboard';
+import 'angular-resource';
+import 'ng-sortable/dist/ng-sortable.js';
+import 'd3';
+import 'c3/c3.js';
+import 'angular-xeditable-npm/dist/js/xeditable.js';
+import 'angular-sanitize';
+import 'angular-animate';
+import 'js-logger';
+import 'lodash';
+import 'angular-route';
+import 'urijs';
+import 'angular-schema-form';
+import 'angular-scrollable-table/angular-scrollable-table.js';
+import 'swagger-ui-dist/swagger-ui-bundle.js';
+import 'swagger-client';
+
+import 'sugar';
+import 'ng-file-upload';
+import 'moment/min/moment.min.js';
+// Markdown editor and code highlight plugin (for editor)
+import '@toast-ui/editor';
+import '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all';
+import 'prismjs';
+import '@toast-ui/editor-plugin-color-syntax';
\ No newline at end of file

MODIFY
manager/ui/war/gulpfile.js
manager/ui/war/gulpfile.js
@@ -61,10 +61,12 @@ gulp.task('default', function() {
// Bundles node_modules required() in ./entry.js to be used on the client (/lib/scripts.js)
gulp.task('browserify', function() {
const allTsFiles = glob.sync('./plugins/api-manager/**/*.ts');
+ const configFiles = ["./apiman/config.js", "./apiman/translations.js"];
+ const allFiles = allTsFiles.concat(configFiles);
return browserify({
basedir: ".",
debug: true,
- entries: ["./entry.ts", "./apiman/config.js", "./apiman/translations.js"].concat(allTsFiles),
+ entries: ["./entry.ts"].concat(allFiles),
cache: {},
packageCache: {}
})

ADD
manager/ui/war/plugins/.gitignore
+*.js.map
\ No newline at end of file

MODIFY
manager/ui/war/plugins/api-manager/html/notifications/notifications-dash.html
manager/ui/war/plugins/api-manager/html/notifications/notifications-dash.html
class="btn btn-default fa fa-check"
role="button"
aria-label="Mark notification read"
- ng-click="markNotificationRead(notification)"
+ ng-click="markRead(notification)"
></button>
<div class="dropdown pull-right dropdown-kebab-pf">
<button
</div>
<div class="list-group-item-text">
<a href="{{ getNotificationMeta(notification).link }}">
- {{ notification.reasonMessage }}
+ {{ getNotificationMeta(notification).message }} {{ notification.id }}
</a>
</div>
</div>
- <div class="list-view-pf-additional-info">
+<!-- <div class="list-view-pf-additional-info">-->
<!-- <div class="list-view-pf-additional-info-item">-->
<!-- <span class="pficon pficon-screen"></span>-->
<!-- <strong>8</strong> Hosts-->
<!-- <span class="pficon pficon-image"></span>-->
<!-- <strong>8</strong> Images-->
<!-- </div>-->
+<!-- </div>-->
</div>
</div>
</div>
</div>
- </div>
+ <uib-pagination boundary-link-numbers="true" direction-links="true" force-ellipses="true"
+ items-per-page="pageSize"
+ max-size="5" ng-change="getNotificationsWithPagination(currentPage, pageSize)"
+ ng-init="currentPage = 1" ng-model="currentPage" rotate="true"
+ total-items="notifications.totalSize"/>
</div>
</div>

ADD
manager/ui/war/plugins/api-manager/ts/model/README.md
+These models have only been added recently (starting 2021) for some components that needed added; they are **not** complete.
+
+The project still has a wide mixture of untyped and typed (likely most still untyped).
\ No newline at end of file

ADD
manager/ui/war/plugins/api-manager/ts/model/api.model.ts
+export interface ApiPlanSummaryBean {
+ planId: string;
+ planName: string;
+ planDescription: string;
+ version: string;
+ exposeInPortal: boolean;
+ requiresApproval: boolean;
+}
+
+export interface ApiGatewayBean {
+ gatewayId: string
+}
+
+export interface ApiPlanBean {
+ planId: string;
+ version: string;
+ exposeInPortal: boolean;
+ requiresApproval: boolean
+}
+
+export interface UpdateApiVersionBean {
+ endpoint: string;
+ endpointType: string; // enum EndpointType
+ endpointContentType: string // enum EndpointContentType;
+ endpointProperties: Map<string, string>;
+ gateways: ApiGatewayBean[]; // Set<ApiGatewayBean>
+ parsePayload: boolean;
+ publicAPI: boolean;
+ disableKeysStrip: boolean;
+ plans: ApiPlanBean[];
+ extendedDescription: string;
+ exposeInPortal: boolean
+}
+
+export interface ApiVersionBean {
+ id: number;
+ //api: ApiBean;
+ status: string; // Enum ApiStatus
+ endpoint: string;
+ endpointType: string; //enum EndpointType
+ endpointContentType: string; // enum EndpointContentType
+ endpointProperties: Map<string, string>;
+ gateways: ApiGatewayBean[];
+ publicAPI: boolean;
+ //apiDefinition: ApiDefinitionBean
+ plans: ApiPlanBean[];
+ version: string;
+ createdBy: string;
+ createdOn: string;
+ modifiedBy: string;
+ modifiedOn: string;
+ publishedOn: string;
+ retiredOn: string;
+ definitionType: string // ApiDefinitionType
+ parsePayload: boolean;
+ disableKeysStrip: boolean;
+ definitionUrl: string;
+ exposeInPortal: boolean;
+ extendedDescription: string
+}

ADD
manager/ui/war/plugins/api-manager/ts/model/blob.model.ts
+export interface BlobRef {
+ id: string;
+ name: string;
+ mimeType: string;
+ hash: number;
+}
\ No newline at end of file

ADD
manager/ui/war/plugins/api-manager/ts/model/checklist.model.ts
+export interface ChecklistConfig {
+ done?: boolean;
+ iconClass?: string;
+ id?: string;
+ name?: string;
+ path?: string;
+ rowClass?: string;
+}
+
+export interface PopoverConfig extends ng.IScope {
+ etarget?: any;
+ autocomplete?: any;
+ tagArray?: any;
+ inputTags?: any;
+ addTag?: any;
+ tagText?: any;
+ inputWidth?: any;
+ defaultWidth?: any;
+ autocompleteFocus?: any;
+ autocompleteSelect?: any;
+ placeholder?: any;
+}
+
+export interface TagInput extends ng.IAttributes {
+ placeholder?: any;
+}
+

ADD
manager/ui/war/plugins/api-manager/ts/model/notifications.model.ts
+import {OrderByBean, PagingBean, SearchCriteriaFilterBean} from "../rpc";
+
+/**
+ * Notifications & events model
+ */
+export interface NotificationCriteriaBean {
+ /**
+ * Allowed values = {
+ * "id",
+ * "category",
+ * "reason",
+ * "reasonMessage",
+ * "status",
+ * "createdOn",
+ * "modifiedOn",
+ * "source"
+ * }
+ */
+ filters?: SearchCriteriaFilterBean[];
+ orderBy?: OrderByBean;
+ paging?: PagingBean;
+}
+
+export interface ApimanEventHeaders {
+ id: string;
+ source: string
+ type: string;
+ subject: string;
+ time: Date;
+ eventVersion: number;
+ otherProperties: Map<string, any>;
+}
+
+export interface IVersionedApimanEvent {
+ headers: ApimanEventHeaders;
+}
+
+export enum NotificationCategory {
+ USER_ADMINISTRATION = 'USER_ADMINISTRATION',
+ API_ADMINISTRATION = 'API_ADMINISTRATION',
+ API_LIFECYCLE = 'API_LIFECYCLE',
+ OTHER = 'OTHER'
+}
+
+export enum NotificationStatus {
+ OPEN = 'OPEN',
+ USER_DISMISSED = 'USER_DISMISSED',
+ SYSTEM_DISMISSED = 'SYSTEM_DISMISSED'
+}
+
+export interface ApimanNotification<P extends IVersionedApimanEvent> {
+ id: number;
+ category: NotificationCategory;
+ reason: string;
+ reasonMessage: string;
+ status: NotificationStatus;
+ createdOn: Date;
+ modifiedOn: Date;
+ source: string;
+ payload: P;
+}
+
+interface UserDto {
+ id: string;
+ username: string;
+ fullName: string;
+ email: string;
+}
+
+export interface ContractCreatedEvent extends IVersionedApimanEvent {
+ headers: ApimanEventHeaders;
+ user: UserDto;
+ clientOrgId: string;
+ clientId: string;
+ clientVersion: string;
+ apiOrgId: string;
+ apiId: string;
+ apiVersion: string;
+ contractId: string;
+ planId: string;
+ planVersion: string;
+ approvalRequired: boolean;
+}
+
+export interface ContractApprovalEvent extends IVersionedApimanEvent {
+ headers: ApimanEventHeaders;
+ approver: UserDto;
+ clientOrgId: string;
+ clientId: string;
+ clientVersion: string;
+ apiOrgId: string;
+ apiId: string;
+ apiVersion: string;
+ contractId: string;
+ planId: string;
+ planVersion: string;
+ approved: boolean;
+ rejectionReason: string;
+}
+
+export interface NotificationAction {
+ markAll?: boolean,
+ notificationIds: number[],
+ status: NotificationStatus
+}
+
+

ADD
manager/ui/war/plugins/api-manager/ts/notification/notification.service.ts
+import { _module } from "../apimanPlugin";
+import {
+ ApimanNotification,
+ NotificationAction,
+ NotificationCriteriaBean,
+ NotificationStatus,
+} from "../model/notifications.model";
+import { SearchResultsBean } from "../rpc";
+
+_module.service("NotificationService", [
+ "$http",
+ "Configuration",
+ "$q",
+ function ($http, Configuration, $q) {
+ return {
+ markNotificationRead: function (notificationIds: number[]): Promise<any> {
+ const userId: string = Configuration.user.username;
+ const notificationAction: NotificationAction = {
+ notificationIds: notificationIds,
+ status: NotificationStatus.USER_DISMISSED,
+ };
+
+ return $http({
+ method: "PUT",
+ url: `${Configuration.api.endpoint}/users/${userId}/notifications`,
+ data: notificationAction,
+ });
+ },
+
+ getNotificationCount: (userId: string): Promise<number> => {
+ return $http({
+ method: "HEAD",
+ url: `${Configuration.api.endpoint}/users/${userId}/notifications`,
+ }).then(
+ (success) => $q.resolve(success.headers("Total-Count")),
+ (failure) => $q.reject(failure)
+ );
+ },
+
+ getNotificationsForUser: (
+ userId: string,
+ criteria: NotificationCriteriaBean
+ ): Promise<SearchResultsBean<ApimanNotification<any>>> => {
+ return $http({
+ method: "POST",
+ url: `${Configuration.api.endpoint}/users/${userId}/notifications`,
+ data: criteria,
+ }).then(
+ (success) => $q.resolve(success.data),
+ (failure) => $q.reject(failure)
+ );
+ },
+ };
+ },
+]);
\ No newline at end of file

ADD
manager/ui/war/plugins/api-manager/ts/notification/notification.ts
+import { _module } from "../apimanPlugin";
+import { PagingBean, SearchResultsBean } from "../rpc";
+import {
+ ApimanNotification,
+ NotificationCriteriaBean,
+} from "../model/notifications.model";
+import { NotificationLayoutMetadata } from "./notificationmapper.service";
+
+_module.controller("Apiman.NotificationController", [
+ "$location",
+ "$scope",
+ "$rootScope",
+ "Logger",
+ "Configuration",
+ "NotificationService",
+ "NotificationMapperService",
+ "PageLifecycle",
+ function (
+ $location,
+ $scope,
+ $rootScope,
+ Logger,
+ Configuration,
+ NotificationService,
+ NotificationMapperService,
+ PageLifecycle
+ ) {
+ $rootScope.pageState = "loaded";
+ $scope.notifications = null;
+ $scope.pageSize = 15;
+
+ PageLifecycle.loadPage(
+ "Notifications",
+ undefined,
+ undefined,
+ $scope,
+ function () {
+ PageLifecycle.setPageTitle("notification.dash", "your notifications");
+ }
+ );
+
+ $scope.getNotificationsWithPagination = (pageIdx: number, pageSize: number): void => {
+ const username: string = Configuration.user.username;
+ const notificationCriteria: NotificationCriteriaBean = {
+ paging: {
+ page: pageIdx,
+ pageSize: pageSize,
+ } as PagingBean,
+ };
+
+ NotificationService.getNotificationsForUser(username, notificationCriteria).then(
+ (searchResult: SearchResultsBean<ApimanNotification<any>>) => {
+ Logger.info("Notifications: {0}", searchResult);
+ $scope.notifications = searchResult;
+ },
+ (failure) => {
+ Logger.error(failure);
+ }
+ )
+ };
+
+ // Load notifs
+ $scope.getNotificationsWithPagination(1, $scope.pageSize);
+
+ // // Load in the notifications
+ // NotificationService.getNotificationsForUser(
+ // Configuration.user.username,
+ // notificationCriteria
+ // ).then(
+ // (searchResult: SearchResultsBean<ApimanNotification<any>>) => {
+ // // TODO: probably should remove this log stmt for perf reasons
+ // Logger.info("Notifications: {0}", searchResult);
+ // $scope.notifications = searchResult;
+ // },
+ // (failure) => {
+ // // TODO: do something useful...
+ // Logger.error(failure);
+ // }
+ // );
+
+ $scope.getNotificationMeta = function (notification: ApimanNotification<any>): NotificationLayoutMetadata {
+ return NotificationMapperService.mapNotification(notification);
+ };
+
+ $scope.goToEventLink = function (notification: ApimanNotification<any>): void {
+ const link = NotificationMapperService.mapNotification(notification).link;
+ $location.path(link);
+ //$scope.$apply();
+ };
+
+ $scope.markRead = function (notification: ApimanNotification<any>): void {
+ const notificationIds: number[] = [ notification.id ];
+ Logger.info("Notification to mark read: {0}", notification);
+ NotificationService.markNotificationRead(notificationIds).then(
+ succeeded => {
+ $scope.notifications.forEach((notification: ApimanNotification<any>, index) => {
+ if (notification.id === notification.id) {
+ this.documents.splice(index, 1);
+ }
+ });
+ },
+ failed => {
+ // TODO: handle gracefully
+ }
+ );
+ };
+ }
+]);

ADD
manager/ui/war/plugins/api-manager/ts/notification/notificationmapper.service.ts
+import {
+ ApimanNotification, ContractApprovalEvent,
+ ContractCreatedEvent,
+} from "../model/notifications.model";
+import { _module } from "../apimanPlugin";
+import { ApimanGlobals } from "../apimanGlobals";
+
+_module.factory("NotificationMapperService", [
+ "$rootScope",
+ ($rootScope) => {
+ return {
+ mapNotification: (
+ notification: ApimanNotification<any>
+ ): NotificationLayoutMetadata => {
+ const result: NotificationLayoutResolver = MAPPER.mapNotification(notification);
+ return {
+ icon: result.icon,
+ reason: result.reason,
+ message: result.messageResolver(notification),
+ link: result.linkResolver(notification),
+ };
+ },
+ };
+ },
+]);
+
+class NotificationMapper {
+ constructor() {}
+
+ public mapNotification(
+ notification: ApimanNotification<any>
+ ): NotificationLayoutResolver {
+ return this.reasonMappings.get(notification.reason);
+ }
+
+ private reasonMappings: Map<String, NotificationLayoutResolver> = new Map<
+ String,
+ NotificationLayoutResolver
+ >([
+ [
+ // http://localhost:2772/api-manager/orgs/test/apis/testy/1.0/contracts
+ // Key
+ "apiman.client.contract.approval.request",
+ // Value
+ {
+ icon: "fa-pencil",
+ reason: "Contract approval request",
+ messageResolver: (notification: ApimanNotification<any>): string => {
+ const notificationWithEvent: ApimanNotification<ContractCreatedEvent> = notification;
+ const event: ContractCreatedEvent = notificationWithEvent.payload;
+ return `A request has been made by ${event.user.username} in org ${event.clientOrgId} to approve access to API ${event.apiId}`
+ },
+ linkResolver: (notification: ApimanNotification<any>): string => {
+ const notificationWithEvent: ApimanNotification<ContractCreatedEvent> = notification;
+ const event: ContractCreatedEvent = notificationWithEvent.payload;
+ return `${ApimanGlobals.pluginName}/orgs/${event.apiOrgId}/apis/${event.apiId}/${event.apiVersion}/contracts`;
+ },
+ },
+ ],
+ [
+ "apiman.client.contract.approval.granted",
+ {
+ icon: "fa-check",
+ reason: "Your API signup request was approved",
+ messageResolver: (notification: ApimanNotification<any>): string => {
+ const notificationWithEvent: ApimanNotification<ContractApprovalEvent> = notification;
+ const event: ContractApprovalEvent = notificationWithEvent.payload;
+ return `Your request to sign up to API ${event.apiId} has been ${this.approvalToString(event)}`;
+ },
+ linkResolver: (notification: ApimanNotification<any>): string => {
+ const notificationWithEvent: ApimanNotification<ContractApprovalEvent> = notification;
+ const event: ContractApprovalEvent = notificationWithEvent.payload;
+ return `${ApimanGlobals.pluginName}/clients/${event.clientOrgId}/clients/${event.clientId}/${event.clientVersion}/contracts`;
+ }
+ },
+ ],
+ ]);
+
+ private approvalToString(event: ContractApprovalEvent): string {
+ return event.approved ? "approved" : "rejected";
+ }
+}
+
+const MAPPER = new NotificationMapper();
+
+export interface NotificationLayoutMetadata {
+ icon: string;
+ reason: string;
+ message: string;
+ link: string;
+}
+
+interface NotificationLayoutResolver {
+ icon: string;
+ reason: string;
+ messageResolver: (notification: ApimanNotification<any>) => string;
+ linkResolver: (notification: ApimanNotification<any>) => string;
+}
\ No newline at end of file

ADD
manager/ui/war/tsconfig.json
+{
+ "compilerOptions": {
+ "module": "CommonJS",
+// "outDir": "dist",
+ "removeComments": true,
+ "preserveConstEnums": true,
+ "sourceMap": true,
+ "noImplicitAny": false, // TODO: Invert this later
+ "target": "ES6"
+// "esModuleInterop": true,
+// "allowSyntheticDefaultImports": false
+ },
+ "files": ["plugins/**/*.ts"],
+ "exclude": ["node_modules"]
+ }
\ No newline at end of file

feat(ui): add notification pane (click bell to access)
feat(ui): add REST service calls for notifications
feat(ui): add interfaces for many Apiman Manager API types

Can click through to the appropriate page for each notification/event received.