Skip to content

Commit b80e4fc

Browse files
Merge branch 'ServiceNowDevProgram:main' into main
2 parents 7d927da + 50feef6 commit b80e4fc

File tree

28 files changed

+1472
-1
lines changed

28 files changed

+1472
-1
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
(function() {
2+
// Purpose: Count how many users hold each licensed role
3+
// Roles: sys_approver, itil, business_stakeholder, admin
4+
5+
var roles = ['sys_approver', 'itil', 'business_stakeholder', 'admin'];
6+
7+
for (var i = 0; i < roles.length; i++) {
8+
var roleName = roles[i];
9+
10+
var ga = new GlideAggregate('sys_user_has_role');
11+
ga.addQuery('role.name', roleName);
12+
ga.addAggregate('COUNT');
13+
ga.query();
14+
15+
if (ga.next()) {
16+
var count = parseInt(ga.getAggregate('COUNT'), 10);
17+
gs.info(roleName + ': ' + count + ' licensed users');
18+
} else {
19+
gs.info(roleName + ': no users found.');
20+
}
21+
}
22+
23+
})();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Licensed User Count by Role Using GlideAggregate
2+
3+
# Overview
4+
This script counts how many **licensed users** hold specific ServiceNow roles using the `GlideAggregate` API.
5+
It’s useful for **license compliance**, **role audits**, and **access management reporting**.
6+
7+
The licensed roles analyzed:
8+
- sys_approver
9+
- itil
10+
- business_stakeholder
11+
- admin
12+
13+
# Objective
14+
To provide a simple, fast, and accurate way to count licensed users per role directly at the database level using `GlideAggregate`.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Scan all Servers (cmdb_ci_server). For each one, check if there is another CI in cmdb_ci_computer with the same name but not a server (sys_class_name != cmdb_ci_server).
2+
3+
If found, log the server name and the duplicate CI’s class; keep a running duplicate count; finally log the total.
4+
5+
*******Descriton****
6+
1. var gr = new GlideRecord("cmdb_ci_server");
7+
2. Creates a record set for Server CIs.
8+
9+
10+
gr.addEncodedQuery("sys_class_name=cmdb_ci_server");
11+
3. Redundant: you’re already targeting the cmdb_ci_server table which is a class table. This filter doesn’t harm, but it’s unnecessary.
12+
13+
14+
while (gr.next()) { ... }
15+
4. Loops through each server CI.
16+
17+
18+
5.Inside loop:
19+
20+
Query cmdb_ci_computer for records with the same name but where sys_class_name != cmdb_ci_server.
21+
6. If found, log the duplicate and increment dupCount.
22+
23+
24+
25+
7. Finally logs total dupCount.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
var dupCount = 0;
2+
var gr = new GlideRecord("cmdb_ci_server");
3+
//gr.addQuery("name", "value");
4+
gr.addEncodedQuery("sys_class_name=cmdb_ci_server");
5+
gr.query();
6+
while (gr.next()) {
7+
var dup = new GlideRecord("cmdb_ci_computer");
8+
dup.addQuery("name", gr.name);
9+
dup.addQuery("sys_class_name", "!=", "cmdb_ci_server");
10+
dup.query();
11+
if (dup.next()) {
12+
gs.log("\t" + gr.name + "\t" + dup.sys_class_name);
13+
dupCount++;
14+
}
15+
16+
}
17+
gs.log("dup count=" + dupCount);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# GlideRecord Field-Level Audit
2+
3+
## Description
4+
This snippet compares two GlideRecord objects field by field and logs all differences.
5+
It is useful for debugging, auditing updates, or validating changes in Business Rules, Script Includes, or Background Scripts.
6+
7+
## Prerequisites
8+
- Server-side context (Background Script, Business Rule, Script Include)
9+
- Two GlideRecord objects representing the original and updated records
10+
- Access to the table(s) involved
11+
12+
## Note
13+
- Works in Global Scope
14+
- Server-side execution only
15+
- Logs all fields with differences to system logs
16+
- Does not modify any records
17+
## Usage
18+
```javascript
19+
// Load original record
20+
var oldRec = new GlideRecord('incident');
21+
oldRec.get('sys_id_here');
22+
23+
// Load updated record
24+
var newRec = new GlideRecord('incident');
25+
newRec.get('sys_id_here');
26+
27+
// Compare and log differences
28+
fieldLevelAudit(oldRec, newRec);
29+
```
30+
31+
## Output
32+
```
33+
Field changed: priority | Old: 5 | New: 2
34+
Field changed: state | Old: 1 | New: 3
35+
Field changed: short_description | Old: 'Old description' | New: 'New description'
36+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Compare two GlideRecord objects field by field and log differences.
3+
*
4+
* @param {GlideRecord} grOld - Original record before changes
5+
* @param {GlideRecord} grNew - Updated record to compare against
6+
*/
7+
function fieldLevelAudit(grOld, grNew) {
8+
if (!grOld || !grNew) {
9+
gs.error('Both old and new GlideRecord objects are required.');
10+
return;
11+
}
12+
13+
var fields = grOld.getFields();
14+
fields.forEach(function(f) {
15+
var name = f.getName();
16+
var oldValue = grOld.getValue(name);
17+
var newValue = grNew.getValue(name);
18+
19+
if (oldValue != newValue) {
20+
gs.info('Field changed: ' + name +
21+
' | Old: ' + oldValue +
22+
' | New: ' + newValue);
23+
}
24+
});
25+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="report-widget-wrap">
2+
3+
<h2 ng-if="c.showTitle" tabindex="0" id="{{'title-' + c.rectangleId }}" class="report-widget-title">{{c.title}}</h2>
4+
5+
<div id="report-widget-{{c.rectangleId}}">
6+
{{::c.initialMessage}}
7+
</div>
8+
</div>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.report-widget-wrap {
2+
background:#fff;
3+
padding:15px;
4+
margin: 0 0 15px 0;
5+
}
6+
7+
.report-widget-title {
8+
padding: $sp-space--xl;
9+
font-weight:bold;
10+
margin-top: 0;
11+
margin-bottom: 0;
12+
font-family: $now-sp-font-family-sans-serif;
13+
color: $text-color;
14+
font-size: $font-size-h4;
15+
16+
}
17+
18+
.highcharts-container g.highcharts-button *,
19+
.highcharts-container image.hc-image {
20+
transition: fill-opacity 0.3s linear, stroke-opacity 0.3s linear, opacity 0.3s linear;
21+
fill-opacity: 0;
22+
stroke-opacity: 0;
23+
opacity:0;
24+
}
25+
26+
.highcharts-container:hover g.highcharts-button *,
27+
.highcharts-container:hover image.hc-image {
28+
fill-opacity: 1;
29+
stroke-opacity: 1;
30+
opacity:1;
31+
}
32+
33+
.highcharts-legend-item span::after,
34+
.highcharts-legend-item::after {
35+
content: "\200E";
36+
}
37+
38+
table.wide .pivot_cell,
39+
table.wide .pivot_caption,
40+
table.wide .pivot_caption_dark {
41+
padding: 3px 5px;
42+
}
43+
.highlight-wrap {
44+
display: none;
45+
}
46+
47+
.fc-week-number {
48+
width: 42px;
49+
background-color: #ededed;
50+
}
51+
52+
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
function($scope, $uibModal, $timeout, spUtil) {
2+
var c = this;
3+
var reportId = c.options.report_id || '';
4+
c.rectangleId = c.widget.rectangle_id || c.data.rectangleId;
5+
c.showTitle = (c.options.show_title === true || c.options.show_title === 'true');
6+
c.title = c.options.title || '';
7+
8+
if (c.options.widget_parameters) {
9+
c.initialMessage = c.data.ch.i18n.building;
10+
window.chartHelpers = window.chartHelpers || {};
11+
$.extend(window.chartHelpers, c.data.ch);
12+
13+
$timeout(function() {
14+
var targetEl = $("#report-widget-" + c.rectangleId);
15+
embedReportById(targetEl, reportId);
16+
17+
$timeout(function() {
18+
targetEl.off('click', 'a[href*="change_request.do?sys_id"]');
19+
20+
targetEl.on('click', 'a[href*="change_request.do?sys_id"]', function(event) {
21+
event.preventDefault();
22+
var href = $(this).attr('href') || '';
23+
var match = href.match(/sys_id=([a-f0-9]{32})/i);
24+
var sysId = match ? match[1] : '';
25+
26+
var modalData = {
27+
number: '',
28+
short_description: '',
29+
description: '',
30+
sys_id: sysId
31+
};
32+
33+
// Open modal immediately
34+
$uibModal.open({
35+
controller: function($scope, $uibModalInstance, $sce) {
36+
$scope.data = modalData;
37+
38+
$scope.getTrustedDescription = function() {
39+
if (!$scope.data.description) return '';
40+
var text = $scope.data.description;
41+
42+
// Convert line breaks to <br>
43+
text = text.replace(/\n/g, '<br>');
44+
45+
// Convert URLs to links
46+
var urlRegex = /(\bhttps?:\/\/[^\s<]+)/gi;
47+
text = text.replace(urlRegex, function(url) {
48+
return '<a href="' + url + '" target="_blank" rel="noopener noreferrer">' + url + '</a>';
49+
});
50+
51+
// Convert emails to mailto links
52+
var emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})/gi;
53+
text = text.replace(emailRegex, function(email) {
54+
return '<a href="mailto:' + email + '">' + email + '</a>';
55+
});
56+
57+
return $sce.trustAsHtml(text);
58+
};
59+
60+
$scope.close = function() {
61+
$uibModalInstance.dismiss('cancel');
62+
};
63+
},
64+
resolve: {
65+
$sce: function() {
66+
return angular.injector(['ng']).get('$sce');
67+
}
68+
},
69+
template: '<div class="modal-header">' +
70+
'<h4 class="modal-title">Change Details</h4>' +
71+
'</div>' +
72+
'<div class="modal-body">' +
73+
'<p><strong>Change Number:</strong> {{data.number}}</p>' +
74+
'<p><strong>Short Description:</strong> {{data.short_description}}</p>' +
75+
'<p><strong>Description:</strong></p>' +
76+
'<p ng-bind-html="getTrustedDescription()"></p>' +
77+
'</div>' +
78+
'<div class="modal-footer">' +
79+
'<a class="btn btn-primary" ng-href="/change_request.do?sys_id={{data.sys_id}}" target="_blank">View Record</a>' +
80+
'<button class="btn btn-default" ng-click="close()">Close</button>' +
81+
'</div>'
82+
});
83+
84+
// Populate modal data via server
85+
spUtil.get(c.widget.sys_id, {
86+
action: 'getChangeDetails',
87+
sys_id: sysId
88+
}).then(function(response) {
89+
if (response.data.changeDetails && !response.data.changeDetails.error) {
90+
modalData.number = response.data.changeDetails.number;
91+
modalData.short_description = response.data.changeDetails.short_description;
92+
modalData.description = response.data.changeDetails.description;
93+
}
94+
});
95+
});
96+
}, 1000);
97+
});
98+
} else {
99+
c.initialMessage = c.data.ch.i18n.selectReport;
100+
}
101+
}

0 commit comments

Comments
 (0)