graphs of performance analytics on a laptop screen
graphs of performance analytics on a laptop screen

Widget - Intégrer des données de ServiceNow dans une feuille Excel ou Google Sheet

15 oct. 2025

Dans ce tutoriel, vous allez découvrir comment importer facilement des données de services now dans un tableur Excel ou dans une Google Sheet.

Pour ce faire nous allons créer un widget qui permet d'extraire des données depuis ServiceNow.

Note: la partie HTML utilise le framework TailwindCSS. Vous devrez importer la librairie avant de pouvoir utiliser le widget.

Server Script

(function() {
	data.tableName = $sp.getParameter('table');
	data.encodedQuery = $sp.getParameter('encodedQuery');
	data.fields = $sp.getParameter('fields');

    if($sp.getParameter('fields')){
		data.fields = $sp.getParameter('fields') + '';
	}
	
	data.records = exportRecords(data.tableName, data.encodedQuery, data.fields);
	
	function exportRecords(table, encodedQuery, fields){
		var fileContent = [];
		
		fields = fields.split(',');
		
		var glideRecord = new GlideRecord(table);
		
		if(!glideRecord.isValid()){
			gs.addErrorMessage('Table is not valid');
			return [];
		}
		
		if(encodedQuery){
			glideRecord.addEncodedQuery(encodedQuery);
		}

        // Check if fields has referenced field (ex: assigned_to.name)
		fields = fields.filter(field => {
			var fieldName = field;
			
			if(fieldName.indexOf('.') > -1){
				fieldName = field.split('.')[0];
			}
			
			return glideRecord.isValidField(fieldName);
		});
      
		glideRecord.query();
      
		while(glideRecord.next()){
			var content = {};

            // Create headers for each columns
			if(fileContent.length == 0){
				var headers = fields.map(field => {
					var label = glideRecord.getElement(field).getLabel();
					
					return label;
				});
				
				fileContent.push(headers);
			}

            // Create rows from glideRecord
			fileContent.push(fields.reduce(function(record, field){
				record.push(glideRecord.getDisplayValue(field));
				
				return record;
			}, []));
		}
		return fileContent;
	}
})();

Client Script

api.controller=function($scope, $location) {
	var c = this;
	$scope.showSettings = false;
	$scope.table = c.data.tableName;
	$scope.encodedQuery = c.data.encodedQuery;
	$scope.fields = c.data.fields;
	$scope.limit = 100;
	
	$scope.headers = [];
	
	if($scope.data.records.length > 0){
		$scope.headers = $scope.data.records[0];
	}
	
	$scope.pages = Math.ceil($scope.data.records.length / $scope.limit);
	$scope.currentPage = 1;
	$scope.records = c.data.records.slice($scope.currentPage, $scope.limit);
	$scope.columns = $scope.fields.split(',');
	
	c.onPreviousPage = function(){
		$scope.currentPage -= 1;
		$scope.records = c.data.records.slice($scope.limit * ($scope.currentPage - 1), $scope.limit * $scope.currentPage);
	}
	c.onNextPage = function(){
		$scope.currentPage += 1;
		$scope.records = c.data.records.slice($scope.limit * ($scope.currentPage - 1), $scope.limit * $scope.currentPage);
	}
	
	c.onCopy = function(){
		var records = c.data.records;
		var output = records.reduce((output, record) => {
			output += record.map(
				data => JSON.stringify(data)
			).join(" \t ") + "\n";
			
			return output;
		}, "");
		
		navigator.clipboard.writeText(output)
			.then(() => alert('Copié !'))
			.catch(() => alert('Échec de la copie.'));	
	}
	
	c.onOpenSettings = function(){
		$scope.showSettings = !$scope.showSettings;
	}
	
	c.onCloseSettings = function(){
		$scope.showSettings = false;
		var queryParams = $location.search();
		
		queryParams['table'] = $scope.table || null;		
		queryParams['encodedQuery'] = $scope.encodedQuery || null;
		queryParams['fields'] = $scope.fields || null;
		
		$location.search(queryParams);
	}
};

HTML

<div class="tw-my-10 tw-font-[Montserrat]">
  <div class="tw-w-full tw-inline-flex tw-justify-between tw-my-4">
    <div>
      <div class="tw-text-5xl">
        ${Export data}
      </div>
      <div class="tw-text-slate-500 tw-mt-2"> 
        {{ c.data.records.length }} items
      </div>
    </div>
    <div>
      <button ng-click="c.onOpenSettings()" class="btn btn-primary">
        ${Settings}
      </button>
      <button ng-click="c.onCopy()" class="btn btn-primary">
        ${Copy}
      </button>
    </div>
  </div>
  <div ng-if="records.length == 0">
    ${No result}
  </div>
  <div class="tw-overflow-x-scroll">
    <table class="tw-w-full tw-bg-white tw-table table-stripped tw-divide-y">
      <thead>
        <tr>
          <th ng-repeat="header in headers" class='tw-px-2 tw-py-2 !tw-font-["The Sans"] tw-font-semibold'>{{ header }}</th>
        </tr>
      </thead>
      <tbody>
        <tr ng-repeat="record in records track by $index">
          <td ng-repeat="data in record" ng-bind-html="data"
              class="tw-px-2 tw-py-2"></td>
        </tr>
      </tbody>
    </table>
  </div>
  <div class="tw-inline-flex tw-items-center tw-justify-center tw-space-x-4 tw-w-full tw-my-4" ng-if="records.length > 0">
    <div>
      <button ng-click="c.onPreviousPage()" ng-disabled="currentPage <= 1"
              class="tw-border-none tw-bg-transparent tw-text-black/70 hover:enabled:tw-text-black disabled:tw-text-black/20">
        <span class="material-icons-outlined">chevron_left</span>
      </button>
    </div>
    <div>
      {{ currentPage }} / {{ pages }}
    </div>
    <div>
      <button ng-click="c.onNextPage()" ng-disabled="currentPage >= c.data.records.length"
              class="tw-border-none tw-bg-transparent tw-text-black/70 hover:enabled:tw-text-black disabled:tw-text-black/20">
        <span class="material-icons-outlined">chevron_right</span>
      </button>
    </div>
  </div>
</div>
<div class="tw-fixed tw-z-[100000] tw-inset-0 tw-h-full tw-w-full tw-bg-black/80" ng-show="showSettings">
  <div class="tw-relative">
    <div class="tw-h-full tw-w-full tw-inline-flex tw0-items-center tw-justify-center tw-py-20">
      <div class="tw-bg-white tw-w-[600px] tw-p-8 tw-rounded-lg">
        <div class="tw-text-3xl tw-mb-4">
          Settings
        </div>
        <div>
          <form class="tw-flex tw-flex-col tw-space-y-4">
            <div>
              <div>
                Table
              </div>
              <div>
                <input ng-model="table" name="table" class="tw-py-2 tw-px-1 tw-border tw-border-solid tw-rounded-lg tw-w-full"/>
              </div>
            </div>
            <div>
              <div>
                Encoded query
              </div>
              <div>
                <input ng-model="encodedQuery" name="encodedQuery" class="tw-py-2 tw-px-1 tw-border tw-border-solid tw-rounded-lg tw-w-full"/>
              </div>
            </div>
            <div>
              <div>
                Fields
              </div>
              <div>
                <input ng-model="fields" name="fields" class="tw-py-2 tw-px-1 tw-border tw-border-solid tw-rounded-lg tw-w-full"/>
              </div>
            </div>
            <div class="tw-w-full tw-inline-flex tw-justify-end">
              <button class="btn btn-primary" ng-click="c.onCloseSettings()">
                Save
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
</div>

Bonne nouvelle

Snowlab est agréé au crédit d'impôt innovation.

Une collaboration Snowlab,
ça vous dit ?

Basés à Bordeaux, nous intervenons partout en France pour des projets ServiceNow ambitieux et humains. Contactez-nous pour échanger sur votre projet et découvrir comment nous pouvons optimiser votre Service Portal ServiceNow !

Bonne nouvelle

Snowlab est agréé au crédit d'impôt innovation.

Une collaboration Snowlab,
ça vous dit ?

Basés à Bordeaux, nous intervenons partout en France pour des projets ServiceNow ambitieux et humains. Contactez-nous pour échanger sur votre projet et découvrir comment nous pouvons optimiser votre Service Portal ServiceNow !