2 |
3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noEmitOnError": false, 5 | "experimentalDecorators": true, 6 | "moduleResolution": "node", 7 | "emitDecoratorMetadata": true, 8 | "removeComments": true, 9 | "lib": [ "es2015", "dom" ], 10 | "sourceMap": true, 11 | "target": "es5", 12 | "declaration": false, 13 | "noImplicitAny": false, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "outDir": "compiler-output" 16 | }, 17 | "exclude": [ 18 | "main.ts", 19 | "node_modules/" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ## Overview 3 | 4 | Brief description of what this PR does, and why it is needed. 5 | 6 | ### Demo 7 | 8 | Optional. Screenshots, `curl` examples, etc. 9 | 10 | ### Notes 11 | 12 | Optional. Ancillary topics, caveats, alternative strategies that didn't work out, anything else. 13 | 14 | ## Testing Instructions 15 | 16 | * How to test this PR 17 | * Prefer bulleted description 18 | * Start after checking out this branch 19 | * Include any setup required, such as bundling scripts, restarting services, etc. 20 | * Include test case, and expected output -------------------------------------------------------------------------------- /src/app/response-status-bar/response-status-bar.component.css: -------------------------------------------------------------------------------- 1 | .ms-MessageBar { 2 | width: 100%; 3 | margin: 0px auto; 4 | font-size: 15px; 5 | margin-top: 15px; 6 | } 7 | 8 | .ms-MessageBar-content { 9 | padding: 6px; 10 | } 11 | .ms-MessageBar-content div { 12 | float: left; 13 | } 14 | .ms-MessageBar-content .ms-MessageBar-actionsOneline { 15 | float: right; 16 | } 17 | .hide-action-bar { 18 | opacity: 0; 19 | } 20 | 21 | .ms-MessageBar-text { 22 | font-size: 14px; 23 | } 24 | 25 | table { 26 | width: 100%; 27 | } 28 | 29 | .ms-Icon.ms-Icon--Cancel { 30 | color: black; 31 | } -------------------------------------------------------------------------------- /src/tsconfig-aot.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "noEmitOnError": false, 5 | "experimentalDecorators": true, 6 | "moduleResolution": "node", 7 | "emitDecoratorMetadata": true, 8 | "removeComments": true, 9 | "lib": [ "es2015", "dom" ], 10 | "sourceMap": true, 11 | "target": "es5", 12 | "declaration": false, 13 | "noImplicitAny": false, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | 18 | "angularCompilerOptions": { 19 | "genDir": "aot", 20 | "skipMetadataEmit" : true 21 | } 22 | } -------------------------------------------------------------------------------- /src/app/method-badge.component.css: -------------------------------------------------------------------------------- 1 | .request-badge { 2 | min-width: 55px; 3 | display: inline-block; 4 | padding: 2px; 5 | text-align: center; 6 | margin-right: 15px; 7 | font-weight: 600; 8 | color: white; 9 | line-height: normal; 10 | padding-bottom: 3px; 11 | } 12 | 13 | .request-badge.GET { 14 | background-color: #000fdf; 15 | } 16 | 17 | .request-badge.POST { 18 | background-color: #008412; 19 | } 20 | 21 | .request-badge.PUT { 22 | background-color: #5C005C; 23 | } 24 | 25 | .request-badge.PATCH { 26 | background-color: #be8b00; 27 | } 28 | 29 | .request-badge.DELETE { 30 | background-color: #a10000; 31 | } -------------------------------------------------------------------------------- /src/app/localization-helpers.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { IExplorerOptions } from './base'; 7 | import { loc_strings } from './loc_strings'; 8 | 9 | export function getString(options: IExplorerOptions, label: string) { 10 | if (label in loc_strings[options.Language]) { 11 | return loc_strings[options.Language][label]; 12 | } 13 | return loc_strings['en-US'][label]; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/method-badge.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { Component, Input } from '@angular/core'; 7 | import { IGraphApiCall } from './base'; 8 | 9 | @Component({ 10 | selector: 'method-badge', 11 | template: ` 12 | {{query.method}} 13 | `, 14 | styleUrls: ['./method-badge.component.css'], 15 | }) 16 | export class MethodBadgeComponent { 17 | @Input() public query: IGraphApiCall; 18 | 19 | public successClass: string; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/bower_components": true, 6 | "**/all.min.js":true 7 | }, 8 | "files.exclude": { 9 | "src/**/*.js": false, 10 | "src/app/*.js": true, 11 | "src/**/.js.map": true, 12 | "src/**/.d.ts": true, 13 | "**/*gulp-tsc*": true, 14 | "**/.git": true, 15 | "**/.DS_Store": true, 16 | // exclude .js and .js.map files, when in a TypeScript project 17 | "**/*.js": { "when": "$(basename).ts"}, 18 | "**/*.js.map": true, 19 | // "**/node_modules": true, 20 | "**/compiler-output": true, 21 | "**/aot": true, 22 | "**/.github": true, 23 | }, 24 | "files.trimTrailingWhitespace": true 25 | } -------------------------------------------------------------------------------- /src/app/sample-categories/sample-categories-panel.component.css: -------------------------------------------------------------------------------- 1 | .category-row { 2 | margin-bottom: 15px; 3 | padding: 0 30px; 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: space-between; 7 | } 8 | 9 | .toggle-control { 10 | display: flex; 11 | width: 160px; 12 | flex-direction: row; 13 | justify-content: space-between; 14 | } 15 | 16 | div.c-toggle button { 17 | margin-top: 0px; 18 | } 19 | 20 | .ms-Panel-headerText { 21 | margin-top: 0px; 22 | margin-bottom: 35px; 23 | } 24 | 25 | .ms-Panel-contentInner { 26 | overflow-y:visible; 27 | } 28 | 29 | .ms-Panel.ms-Panel--lg { 30 | max-width: 544px; 31 | overflow-y: auto; 32 | } 33 | 34 | div.c-toggle button:focus { 35 | outline: 2px solid blue; 36 | } 37 | 38 | /* core.css frontdoor conflict */ 39 | .category-switch button { 40 | min-width: inherit; 41 | } -------------------------------------------------------------------------------- /rollup-config.js: -------------------------------------------------------------------------------- 1 | import rollup from 'rollup' 2 | import nodeResolve from 'rollup-plugin-node-resolve' 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import uglify from 'rollup-plugin-uglify' 5 | 6 | export default { 7 | entry: 'src/main.js', 8 | dest: 'dist/explorer.js', // output a single application bundle 9 | sourceMap: true, 10 | format: 'iife', 11 | onwarn: function (warning) { 12 | // Skip certain warnings 13 | 14 | // should intercept ... but doesn't in some rollup versions 15 | if (warning.code === 'THIS_IS_UNDEFINED') { return; } 16 | 17 | // console.warn everything else 18 | console.warn(warning.message); 19 | }, 20 | plugins: [ 21 | nodeResolve({ jsnext: true, module: true }), 22 | commonjs({ 23 | include: [ 24 | 'node_modules/rxjs/**', 25 | 'node_modules/guid-typescript/dist/guid.js' 26 | ] 27 | }), 28 | uglify() 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/app/share-link/share-link-btn.component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 |
5 | -------------------------------------------------------------------------------- /src/app/opt-in-out-banner/banner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { GraphExplorerComponent } from '../GraphExplorerComponent'; 3 | 4 | @Component({ 5 | selector: 'banner', 6 | templateUrl: './banner.component.html', 7 | styleUrls: ['./banner.component.css'], 8 | }) 9 | export class BannerComponent extends GraphExplorerComponent { 10 | public successClass = 'ms-Toggle-field'; 11 | 12 | constructor() { 13 | super(); 14 | } 15 | 16 | public switchToggle() { 17 | this.successClass = 'ms-Toggle-field is-selected'; 18 | const path = location.href; 19 | const urlObject: URL = new URL(path); 20 | const { protocol, hostname, pathname, port } = urlObject; 21 | let url = `${protocol}//${hostname}${(port) ? ':' + port : ''}${pathname}`; 22 | url = url.replace(/\/$/, ''); 23 | window.location.href = url.includes('localhost') ? 'http://localhost:3001' : `${url}/preview`; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/request-editors.component.css: -------------------------------------------------------------------------------- 1 | #post-body-editor { 2 | position: relative; 3 | height: 20vh; 4 | border: 1px solid #ccc; 5 | margin-top: 10px; 6 | } 7 | 8 | .header-row input { 9 | width: 95%; 10 | margin-top: 5px; 11 | } 12 | 13 | table { 14 | width: 100%; 15 | } 16 | 17 | th { 18 | text-align: left; 19 | font-weight: 300; 20 | } 21 | 22 | td.remove-header-btn { 23 | font-size: 20px; 24 | } 25 | 26 | td.remove-header-btn:hover { 27 | cursor: pointer; 28 | } 29 | 30 | td.remove-header-btn i { 31 | margin-top: 12px; 32 | font-size: 20px; 33 | } 34 | 35 | .invisible { 36 | opacity: 0; 37 | } 38 | 39 | .header-autocomplete { 40 | max-width: inherit; 41 | margin: 0px; 42 | height: inherit; 43 | } 44 | 45 | .c-menu.f-auto-suggest-no-results { 46 | display: none; 47 | } 48 | 49 | .ms-Pivot-content { 50 | margin-top: 10px; 51 | } 52 | 53 | .ms-Pivot-link:hover, .ms-Pivot-link:focus { 54 | background: rgba(0,0,0,0.25); 55 | outline: 2px solid white; 56 | } -------------------------------------------------------------------------------- /assets/images/history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/canary/canary.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { Component } from '@angular/core'; 7 | import { localLogout } from '../authentication/auth'; 8 | import { GraphExplorerComponent } from '../GraphExplorerComponent'; 9 | 10 | @Component({ 11 | selector: 'canary', 12 | styleUrls: ['./canary.component.css'], 13 | templateUrl: './canary.component.html', 14 | }) 15 | export class CanaryComponent extends GraphExplorerComponent { 16 | 17 | constructor() { 18 | super(); 19 | } 20 | 21 | public disableCanary() { 22 | localStorage.setItem('GRAPH_MODE', null); 23 | localStorage.setItem('GRAPH_URL', 'https://graph.microsoft.com'); 24 | localLogout(); 25 | location.reload(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | microsoft-graph-explorer 2 | 3 | Copyright (c) Microsoft Corporation 4 | All rights reserved.  5 | MIT License 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /src/app/queryrow/queryrow.component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 | 6 |
{{getQueryText()}}
7 | 9 | 10 | 11 | 13 | 14 | 15 |
16 | 17 |
-------------------------------------------------------------------------------- /src/app/scopes-dialog/scopes-dialog.component.css: -------------------------------------------------------------------------------- 1 | #scopes-dialog { 2 | min-width: 800px; 3 | } 4 | 5 | #scopes-list-table-container { 6 | max-height: 451px; 7 | overflow: auto; 8 | margin-top: 20px; 9 | } 10 | 11 | .ms-Dialog { 12 | max-width: 770px; 13 | z-index: 999; 14 | } 15 | 16 | .ms-Dialog-title { 17 | text-transform: capitalize; 18 | } 19 | 20 | .ms-Link { 21 | color: #0078d7; 22 | } 23 | 24 | .ms-CheckBox-field:before, .ms-CheckBox-field:after { 25 | margin-top: 4px; 26 | } 27 | 28 | .ms-MessageBar-text { 29 | font-size: 15px; 30 | } 31 | 32 | .ms-MessageBar { 33 | margin-top: 20px; 34 | width: 100%; 35 | height: 40px; 36 | } 37 | 38 | .c-checkbox input[type=checkbox]:focus+span:before { 39 | outline: 2px solid #0078d7; 40 | } 41 | 42 | label.c-label { 43 | margin-top: 10px; 44 | margin-bottom: 10px; 45 | } 46 | 47 | .preview-label { 48 | margin-left: 10px; 49 | } 50 | 51 | .consented-label { 52 | color: forestgreen; 53 | margin-left: 10px; 54 | } 55 | 56 | .scopeRow:hover, .scopeRow:focus { 57 | background: #fff; 58 | border: 1px solid rgba(0,0,0,0.25); 59 | } 60 | -------------------------------------------------------------------------------- /src/app/history/history-panel.component.css: -------------------------------------------------------------------------------- 1 | .status-code.success { 2 | color: #006300; 3 | } 4 | 5 | .status-code.error { 6 | color: #9c0028; 7 | } 8 | 9 | tr.request-history-query:hover td:not(.remove-query) { 10 | background-color: #f4f4f4; 11 | cursor: pointer; 12 | } 13 | 14 | td.export-query:hover { 15 | color: #000fdf; 16 | text-decoration: underline; 17 | } 18 | 19 | td.remove-query i { 20 | opacity: 0; 21 | } 22 | 23 | tr.request-history-query:hover td.remove-query i { 24 | opacity: 1; 25 | } 26 | 27 | td.duration { 28 | text-align: right; 29 | } 30 | 31 | tr.request-history-query:hover td.remove-query { 32 | cursor: pointer; 33 | text-align: center; 34 | } 35 | 36 | td.remove-query:hover { 37 | background-color: #e4e4e4; 38 | } 39 | 40 | #panel-actions { 41 | position: absolute; 42 | bottom: 0px; 43 | width: 100%; 44 | background: white; 45 | padding: 15px; 46 | text-align: right; 47 | padding-right: 80px; 48 | } 49 | 50 | 51 | tr.request-history-query.restrict:hover td { 52 | cursor: not-allowed !important; 53 | } 54 | 55 | td.relative-date { 56 | white-space: nowrap; 57 | } -------------------------------------------------------------------------------- /src/app/ApiCallDisplayHelpers.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { AppComponent } from './app.component'; 7 | import { AllowedGraphDomains, IGraphApiCall } from './base'; 8 | import { getString } from './localization-helpers'; 9 | 10 | export function getShortQueryText(query: IGraphApiCall) { 11 | let shortQueryUrl; 12 | 13 | if (query.requestUrl) { 14 | // Parse out /v1.0/me from graph.microsoft.com/v1.0/me for all domains 15 | for (const GraphDeploymentUrl of AllowedGraphDomains) { 16 | if (query.requestUrl.startsWith(GraphDeploymentUrl)) { 17 | shortQueryUrl = query.requestUrl.split(GraphDeploymentUrl)[1]; 18 | break; 19 | } 20 | } 21 | } 22 | 23 | const queryText = query.humanName || shortQueryUrl; 24 | 25 | return (getString(AppComponent.Options, queryText)) || queryText; 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch current node file", 11 | "program": "${file}" 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "name": "Launch Program", 17 | "program": "${workspaceRoot}\\serve\"", 18 | "outFiles": [] 19 | }, 20 | { 21 | "type": "node", 22 | "request": "attach", 23 | "name": "Attach to Port", 24 | "address": "localhost", 25 | "port": 3000, 26 | "outFiles": [] 27 | }, 28 | { 29 | "name": "Attach localhost", 30 | "type": "chrome", 31 | "request": "attach", 32 | "port": 9222, 33 | "url": "http://localhost:3000/", 34 | "webRoot": "${workspaceRoot}" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /src/systemjs-angular-loader.js: -------------------------------------------------------------------------------- 1 | var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*)/gm; 2 | var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g; 3 | var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g; 4 | 5 | module.exports.translate = function(load){ 6 | var url = document.createElement('a'); 7 | url.href = load.address; 8 | 9 | var basePathParts = url.pathname.split('/'); 10 | 11 | basePathParts.pop(); 12 | var basePath = basePathParts.join('/'); 13 | 14 | var baseHref = document.createElement('a'); 15 | baseHref.href = this.baseURL; 16 | baseHref = baseHref.pathname; 17 | 18 | basePath = basePath.replace(baseHref, ''); 19 | 20 | load.source = load.source 21 | .replace(templateUrlRegex, function(match, quote, url){ 22 | let resolvedUrl = url; 23 | 24 | if (url.startsWith('.')) { 25 | resolvedUrl = basePath + url.substr(1); 26 | } 27 | 28 | return 'templateUrl: "' + resolvedUrl + '"'; 29 | }) 30 | .replace(stylesRegex, function(match, relativeUrls) { 31 | var urls = []; 32 | 33 | while ((match = stringRegex.exec(relativeUrls)) !== null) { 34 | if (match[2].startsWith('.')) { 35 | urls.push('"' + basePath + match[2].substr(1) + '"'); 36 | } else { 37 | urls.push('"' + match[2] + '"'); 38 | } 39 | } 40 | 41 | return "styleUrls: [" + urls.join(', ') + "]"; 42 | }); 43 | 44 | return load; 45 | }; 46 | -------------------------------------------------------------------------------- /util-scripts/bundle-loc-files.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. 3 | // ------------------------------------------------------------------------------ 4 | 5 | 6 | // Bundles files in translation_files/ to scripts/loc_strings.js 7 | 8 | 9 | var fs = require('fs'); 10 | 11 | fs.readdir("translation_files", function(err, filenames) { 12 | if (err) { 13 | onError(err); 14 | return; 15 | } 16 | var fileStr = '// This is a generated file from bundleLocFiles.js \n\nexport const loc_strings = {};' 17 | var languageRead = []; 18 | filenames.forEach(function(filename) { 19 | languageRead.push(new Promise(function(resolve) { 20 | fs.readFile("translation_files/" + filename, 'utf-8', function(err, content) { 21 | fileStr += '\n\n' + 'loc_strings[\'' + filename.split(".")[0] + '\'] = ' + content; 22 | resolve(); 23 | }); 24 | })) 25 | }); 26 | Promise.all(languageRead).then(() => { 27 | fs.writeFile("src/app/loc_strings.ts", fileStr, function(err) { 28 | if(err) { 29 | return console.log(err); 30 | } 31 | 32 | console.log("The file was saved!"); 33 | }); 34 | }) 35 | }); -------------------------------------------------------------------------------- /src/app/authentication/authentication.component.css: -------------------------------------------------------------------------------- 1 | #ms-signin-button-container { 2 | width: 100%; 3 | text-align: center; 4 | } 5 | 6 | #ms-signin-button { 7 | width: 215px; 8 | height: 40px; 9 | margin: 20px 0 0px 0px; 10 | border: none; 11 | cursor: pointer; 12 | display: inline-block; 13 | color: black; 14 | text-align: center; 15 | font-family:"Segoe UI","wf_segoe-ui_normal","Arial",sans-serif; 16 | font-size: 15px; 17 | background: url('http://graphstagingblobstorage.blob.core.windows.net/staging/vendor/bower_components/explorer/assets/images/MSSignInButton.png') no-repeat; 18 | background-size: 100%; 19 | border-spacing: 5px; 20 | } 21 | 22 | [tabindex]:focus { 23 | outline: 1px dashed #fff; 24 | } 25 | 26 | .button-text { 27 | padding-left: 20px; 28 | } 29 | 30 | #signout { 31 | float: right; 32 | padding-right: 16px; 33 | color: #00bcf2; 34 | } 35 | 36 | #userDisplayName { 37 | color: #e2e2e2 38 | } 39 | 40 | #userMail { 41 | color: #a0a0a0; 42 | } 43 | 44 | #authenticating-progress-bar { 45 | margin: 0px auto; 46 | } 47 | 48 | .noPicture .ms-Persona-details { 49 | padding-left: 0px; 50 | } 51 | 52 | #manage-permissions { 53 | float: left; 54 | color: #00bcf2; 55 | } 56 | 57 | #signout, #manage-permissions { 58 | margin-top: 9px; 59 | } 60 | 61 | #persona-holder { 62 | margin-top: 15px; 63 | margin-bottom: -15px; 64 | } 65 | -------------------------------------------------------------------------------- /src/app/response-status-bar/response-status-bar.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { Component, Input } from '@angular/core'; 7 | import { DomSanitizer } from '@angular/platform-browser'; 8 | import { IMessageBarContent } from '../base'; 9 | import { GraphExplorerComponent } from '../GraphExplorerComponent'; 10 | 11 | @Component({ 12 | selector: 'message-bar', 13 | templateUrl: './response-status-bar.component.html', 14 | styleUrls: ['./response-status-bar.component.css'], 15 | }) 16 | export class ResponseStatusBarComponent extends GraphExplorerComponent { 17 | 18 | @Input() public message: IMessageBarContent; 19 | 20 | public messageHTML: string; 21 | 22 | constructor(private sanitizer: DomSanitizer) { 23 | super(); 24 | } 25 | 26 | public ngOnChanges() { 27 | this.setMessageText(); 28 | } 29 | 30 | public clearMessage() { 31 | this.message = null; 32 | } 33 | 34 | public setMessageText() { 35 | if (this.message) { 36 | this.messageHTML = this.sanitizer.bypassSecurityTrustHtml(this.message.text) as string; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | pool: 2 | vmImage: 'vs2017-win2016' 3 | 4 | steps: 5 | - task: NodeTool@0 6 | inputs: 7 | versionSpec: '8.x' 8 | displayName: 'Install Node.js' 9 | 10 | - script: | 11 | npm install 12 | displayName: 'npm install' 13 | - script: | 14 | npm run lint 15 | displayName: 'Runs linting checks' 16 | 17 | - script: | 18 | npm run import:samples 19 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/dev')) 20 | displayName: 'Imports samples' 21 | 22 | - script: | 23 | npm test 24 | displayName: 'Runs tests' 25 | 26 | - script: | 27 | npm run import:loc-strings 28 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/dev')) 29 | displayName: 'Imports loc-strings' 30 | 31 | - script: | 32 | npm run build:prod 33 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/dev')) 34 | displayName: 'Runs a production build' 35 | 36 | 37 | - task: AzureFileCopy@2 38 | inputs: 39 | sourcePath: 'dist' 40 | azureConnectionType: 'ConnectedServiceNameARM' 41 | azureSubscription: 'arm-connection' 42 | destination: 'azureBlob' 43 | storage: 'graphstagingblobstorage' 44 | containerName: 'staging/vendor/bower_components/explorer/dist' 45 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/dev')) 46 | displayName: 'Stage' 47 | 48 | -------------------------------------------------------------------------------- /src/app/graph-service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@angular/http'; 2 | 3 | import { 4 | inject, 5 | TestBed, 6 | } from '@angular/core/testing'; 7 | 8 | import { GraphApiVersions } from './base'; 9 | import { GraphService } from './graph-service'; 10 | 11 | let graphService: GraphService; 12 | describe('Metadata download and parsing', () => { 13 | 14 | beforeEach(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [HttpModule], 17 | providers: [GraphService ], 18 | }); 19 | graphService = TestBed.get(GraphService); 20 | }); 21 | 22 | // tslint:disable-next-line 23 | it('Creates an instance of the graph service', inject([GraphService], (_graphService: GraphService) => { 24 | graphService = _graphService; 25 | })); 26 | 27 | for (const version of GraphApiVersions) { 28 | it(`should download ${version} metadata`, (done) => { 29 | graphService.getMetadata('https://graph.microsoft.com', version).then(done); 30 | }); 31 | } 32 | 33 | it('should error on downloading v5.x metadata', (done) => { 34 | graphService.getMetadata('https://graph.microsoft.com', '5.x').then(() => { 35 | done.fail('Downloaded invalid metadata'); 36 | }).catch(done); 37 | }); 38 | 39 | it('should download canary metadata', (done) => { 40 | graphService.getMetadata('https://canary.graph.microsoft.com', '1.0').then(() => { 41 | done.fail('Downloaded invalid metadata'); 42 | }).catch(done); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/app/aria-selected.directive.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | /* 7 | AriaSelectedMSPivotLinkDirective - An attribute directive that uses the ms-Pivot-link class selector to 8 | to apply the aria-selected attribute and value when the tab gains and loses focus. 9 | https://dev.office.com/fabric-js/Components/Pivot/Pivot.html 10 | */ 11 | import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core'; 12 | @Directive({ 13 | selector: '.ms-Pivot-link', 14 | }) 15 | export class AriaSelectedMSPivotLinkDirective { 16 | constructor(private el: ElementRef, private renderer: Renderer2) { 17 | this.el = el; 18 | this.renderer = renderer; 19 | } 20 | 21 | // Set the aria-selected attribute to true when the tab (ms-pivot-link) gains focus. 22 | @HostListener('focus') 23 | public onFocus() { 24 | this.renderer.setAttribute(this.el.nativeElement, 'aria-selected', 'true'); 25 | } 26 | 27 | // Set the aria-selected attribute to false when the tab (ms-pivot-link) loses focus. 28 | @HostListener('blur') 29 | public onBlur() { 30 | this.renderer.setAttribute(this.el.nativeElement, 'aria-selected', 'false'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/fabric-components.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | declare let fabric: any; 7 | export function initFabricComponents() { 8 | const PivotElements = document.querySelectorAll('.ms-Pivot'); 9 | for (let i = 0; i < PivotElements.length; i++) { // tslint:disable-line 10 | new fabric['Pivot'](PivotElements[i]); // tslint:disable-line 11 | } 12 | if ('Spinner' in fabric) { 13 | const elements = document.querySelectorAll('.ms-Spinner'); 14 | let i = elements.length; 15 | let component; // tslint:disable-line 16 | while (i--) { 17 | component = new fabric.Spinner(elements[i]); 18 | } 19 | } 20 | 21 | const DialogElements = document.querySelectorAll('.ms-Dialog'); 22 | const DialogComponents = []; 23 | for (let i = 0; i < DialogElements.length; i++) { 24 | ((() => { 25 | DialogComponents[i] = new fabric.Dialog(DialogElements[i]); 26 | })()); 27 | } 28 | const TableElements = document.querySelectorAll('.ms-Table'); 29 | for (let i = 0; i < TableElements.length; i++) { // tslint:disable-line 30 | new fabric['Table'](TableElements[i]); // tslint:disable-line 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/history/history.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { AppComponent } from '../app.component'; 7 | import { IGraphApiCall } from '../base'; 8 | 9 | const LocalStorageKeyGraphRequestHistory = 'GRAPH_V4.1_REQUEST_HISTORY'; 10 | 11 | export function saveHistoryToLocalStorage(requestHistory: IGraphApiCall[]) { 12 | try { 13 | localStorage.setItem(LocalStorageKeyGraphRequestHistory, JSON.stringify(requestHistory)); 14 | } catch (e) { 15 | AppComponent.messageBarContent = { 16 | backgroundClass: 'ms-MessageBar--warning', 17 | icon: 'ms-Icon--Info', 18 | text: 'You have reached the browser storage limit,' + 19 | ' click show more under the history heading to remove items.', 20 | }; 21 | } 22 | } 23 | 24 | export function deleteHistoryFromLocalStorage() { 25 | localStorage.setItem(LocalStorageKeyGraphRequestHistory, JSON.stringify([])); 26 | } 27 | 28 | export function loadHistoryFromLocalStorage(): IGraphApiCall[] { 29 | const possibleHistory = localStorage.getItem(LocalStorageKeyGraphRequestHistory); 30 | 31 | if (!possibleHistory) { 32 | return []; 33 | } 34 | 35 | return JSON.parse(possibleHistory); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/history/har/harUtil.ts: -------------------------------------------------------------------------------- 1 | import { IHarFormat, IHarPayload } from './IHarFormat'; 2 | 3 | export function generateHar(payload: IHarPayload): IHarFormat { 4 | return { 5 | log: { 6 | version: '1.0', 7 | creator: { 8 | name: 'Graph Explorer', 9 | version: '1.0', 10 | }, 11 | entries: [ 12 | { 13 | startedDateTime: payload.startedDateTime, 14 | time: payload.time, 15 | request: { 16 | method: payload.method, 17 | url: payload.url, 18 | httpVersion: payload.httpVersion, 19 | cookies: payload.cookies, 20 | headers: payload.request.headers, 21 | queryString: payload.queryString, 22 | postData: payload.postData, 23 | headersSize: -1, 24 | bodySize: -1, 25 | }, 26 | response: { 27 | status: payload.status, 28 | statusText: payload.statusText, 29 | httpVersion: payload.httpVersion, 30 | cookies: payload.cookies, 31 | headers: payload.response.headers, 32 | content: payload.content, 33 | redirectURL: '', 34 | headersSize: -1, 35 | bodySize: -1, 36 | }, 37 | cache: {}, 38 | timings: { 39 | send: payload.sendTime, 40 | wait: payload.waitTime, 41 | receive: payload.receiveTime, 42 | }, 43 | connection: '', 44 | }, 45 | ], 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/util.ts: -------------------------------------------------------------------------------- 1 | import { Headers } from '@angular/http'; 2 | import { IGraphRequestHeader } from './base'; 3 | 4 | export function createHeaders(explorerHeaders: IGraphRequestHeader[]): Headers { 5 | const h = new Headers(); 6 | 7 | for (const header of explorerHeaders) { 8 | if (!header.name) { 9 | continue; 10 | } 11 | /* 12 | Handle backslash that is returned in odata.etag before double-quote 13 | as the etag would otherwise be invalid and request will fail 14 | if user just does copy-paste odata.etag value from the previous response 15 | */ 16 | if (header.name === 'If-Match') { 17 | h.append(header.name, header.value.replace(/\\"/g, '"')); 18 | } else { 19 | h.append(header.name, header.value); 20 | } 21 | } 22 | 23 | return h; 24 | } 25 | 26 | /* 27 | https://github.com/Microsoft/rDSN/blob/f1f474da71003b72f445dcebd6638768301ce930/src/tools/webstudio/app_package 28 | /static/js/analyzer.js#L2 29 | */ 30 | export function getParameterByName(name: string) { 31 | name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); 32 | const regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); 33 | const results = regex.exec(location.search); 34 | return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); 35 | } 36 | 37 | export function getGraphUrl() { 38 | const graphUrl = localStorage.getItem('GRAPH_URL'); 39 | if (graphUrl) { 40 | return graphUrl; 41 | } 42 | return 'https://graph.microsoft.com'; 43 | } 44 | -------------------------------------------------------------------------------- /src/app/sample-categories/sample-categories-panel.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 |

{{getStr('Sample Categories')}}

9 |
10 |
12 | {{getStr(category.title)}} ({{category.queries.length}}) 13 |
14 |
15 |
16 | 18 |
19 |
20 | {{category.enabled? getStr('On') : getStr('Off')}} 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /src/app/main-column/main-column.component.css: -------------------------------------------------------------------------------- 1 | #request-bar-row-form { 2 | display: flex; 3 | flex-wrap: wrap; 4 | margin-top: -5px; 5 | } 6 | 7 | #request-bar-row-form::after { 8 | content: ''; 9 | width: 100%; 10 | } 11 | 12 | .c-select.f-border { 13 | min-width: inherit; 14 | } 15 | 16 | .c-select:after { 17 | display: none; 18 | } 19 | 20 | #responseImg { 21 | max-width: 300px; 22 | } 23 | 24 | #graph-request-url { 25 | flex: 1; 26 | margin-right: 8px; 27 | max-width: 100%; 28 | } 29 | 30 | #submitBtn { 31 | height: 32px; 32 | margin-top: 20px; 33 | padding-top: 6px; 34 | } 35 | 36 | .ms-Spinner { 37 | margin-left: 38px; 38 | position: relative; 39 | top: -1px; 40 | } 41 | 42 | #spacer-1 { 43 | margin-bottom: 50px; 44 | } 45 | 46 | button.c-button[type=submit]:focus:not(.x-hidden-focus) { 47 | outline: #000 solid 1px !important; 48 | } 49 | 50 | 51 | .c-auto-suggest .c-menu, .m-auto-suggest .c-menu { 52 | max-width: 100%; 53 | } 54 | .c-menu.f-auto-suggest-no-results { 55 | display: none; 56 | } 57 | 58 | .c-menu.f-auto-suggest-scroll { 59 | max-height: 300px !important; 60 | } 61 | 62 | #go-lightning-icon { 63 | position: relative; 64 | top: 2px; 65 | } 66 | 67 | 68 | /*mobile*/ 69 | 70 | 71 | @media (max-width: 639px) { 72 | .bump-flex-row-mobile { 73 | order: 1; 74 | margin: 0px auto; 75 | margin-top: 20px; 76 | } 77 | } 78 | 79 | :host /deep/ button { 80 | line-height: 1; 81 | } 82 | 83 | .ms-Pivot-link:hover, .ms-Pivot-link:focus { 84 | background: rgba(0,0,0,0.25); 85 | outline: 2px solid white; 86 | } -------------------------------------------------------------------------------- /src/app/generic-message-dialog.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { Component } from '@angular/core'; 7 | import { AppComponent } from './app.component'; 8 | import { GraphExplorerComponent } from './GraphExplorerComponent'; 9 | 10 | declare let fabric; 11 | 12 | @Component({ 13 | selector: 'generic-dialog', 14 | styles: [` 15 | 16 | `], 17 | template: ` 18 |
19 |
20 | 23 |
{{getMessage()?.title}}
24 |
25 | {{getMessage()?.body}} 26 |
27 |
28 | 31 |
32 |
33 |
34 | 35 | `, 36 | }) 37 | export class GenericDialogComponent extends GraphExplorerComponent { 38 | public static showDialog() { 39 | const el = document.querySelector('#message-dialog'); 40 | const fabricDialog = new fabric.Dialog(el); 41 | fabricDialog.open(); 42 | } 43 | 44 | public getMessage() { 45 | return AppComponent.message; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/queryrow/queryrow.component.css: -------------------------------------------------------------------------------- 1 | .api-query:hover, .c-drawer>button:hover, .api-query:focus, .c-drawer>button:focus, .query-link:focus { 2 | background: rgba(0,0,0,0.25); 3 | } 4 | 5 | .query-link { 6 | color: white; 7 | padding: 6px; 8 | margin: 3px, 0px, 3px, 0px; 9 | display: block; 10 | float: right; 11 | } 12 | 13 | .api-query:hover .query-link { 14 | background-color: #232323; 15 | } 16 | 17 | .query-link:hover { 18 | background: rgba(0,0,0,0.4) !important; 19 | cursor: pointer; 20 | } 21 | 22 | .restrict:hover { 23 | cursor: not-allowed; 24 | } 25 | 26 | .api-query { 27 | cursor: pointer; 28 | font-size: 13px; 29 | line-height: 16px; 30 | display: block; 31 | border: 0; 32 | background: 0 0; 33 | font-weight: 500; 34 | padding: 0px 5px 0px 12px; 35 | left: 0; 36 | text-align: left; 37 | width: 100%; 38 | overflow: hidden; 39 | white-space: nowrap; 40 | text-overflow: ellipsis; 41 | margin-left: -12px; 42 | } 43 | 44 | .row-1 { 45 | display: flex; 46 | flex-wrap: wrap; 47 | } 48 | 49 | .duration { 50 | float: right; 51 | } 52 | 53 | .query { 54 | flex: 1; 55 | overflow: hidden; 56 | text-overflow: ellipsis; 57 | padding: 2px; 58 | margin: auto; 59 | color: white; 60 | height: 20px; 61 | } 62 | 63 | method-badge { 64 | margin:auto; 65 | height: 22px; 66 | } 67 | 68 | .query-link:hover .ms-Icon--Page:before { 69 | content: '\E729'; 70 | } 71 | 72 | .query:hover { 73 | text-decoration: underline; 74 | color: #fff; 75 | } 76 | -------------------------------------------------------------------------------- /src/app/api-explorer-jseditor.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | declare const ace; 7 | 8 | export function getRequestBodyEditor() { 9 | return getAceEditorFromElId('post-body-editor'); 10 | } 11 | 12 | export function getJsonViewer() { 13 | return getAceEditorFromElId('jsonViewer'); 14 | } 15 | 16 | export function getAceEditorFromElId(id: string) { 17 | return ace.edit(document.getElementById(id)); 18 | } 19 | 20 | /** 21 | * Initialize the text editor. 22 | * @param editor The AceEditor component 23 | * @param ariaLabel The aria-label attribute to add to the textarea of the AceEditor. 24 | * @param text Default text to add to editor. 25 | */ 26 | export function initializeAceEditor(editor, ariaLabel: string, text?: string) { 27 | commonAceSetup(editor); 28 | 29 | if (text) { 30 | editor.getSession().insert(0, text); 31 | } 32 | 33 | editor.moveCursorTo(1, 0); 34 | editor.textInput.getElement().setAttribute('name', ariaLabel); 35 | } 36 | 37 | /** 38 | * Standard ace editor setup 39 | * @param editor The AceEditor component 40 | */ 41 | export function commonAceSetup(editor) { 42 | editor.setShowPrintMargin(false); 43 | editor.$blockScrolling = Infinity; 44 | editor.renderer.setOption('showLineNumbers', false); 45 | 46 | // Accessibility - keyboard dependant users must be able to "tab out" of session 47 | editor.commands.bindKey('Tab', null); 48 | editor.commands.bindKey('Shift-Tab', null); 49 | } 50 | -------------------------------------------------------------------------------- /src/app/request-editors/monaco-editor.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | declare let monaco: any; 7 | 8 | export function initializeEditor() { 9 | const headersViewer = monaco.Uri.parse(`a://b/response-header-viewer.json`); 10 | const bodyEditor = monaco.Uri.parse(`a://b/body-editor.json`); 11 | const resultsViewer = monaco.Uri.parse(`a://b/results-viewer.json`); 12 | 13 | const headersModel = monaco.editor.createModel('', 'plain_text', headersViewer); 14 | const bodyModel = monaco.editor.createModel('', 'json', bodyEditor); 15 | const resultsModel = monaco.editor.createModel('', 'json', resultsViewer); 16 | 17 | (window as any).headersViewer = monaco.editor.create(document.getElementById('response-header-viewer'), { 18 | model: headersModel, 19 | minimap: { 20 | enabled: false, 21 | }, 22 | readOnly: true, 23 | automaticLayout: true, 24 | }); 25 | 26 | (window as any).bodyEditor = monaco.editor.create(document.getElementById('body-editor'), { 27 | model: bodyModel, 28 | minimap: { 29 | enabled: false, 30 | }, 31 | automaticLayout: true, 32 | }); 33 | 34 | (window as any).resultsViewer = monaco.editor.create(document.getElementById('results-viewer'), { 35 | readOnly: true, 36 | minimap: { 37 | enabled: false, 38 | }, 39 | model: resultsModel, 40 | automaticLayout: true, 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/app/response-status-bar/response-status-bar.component.html: -------------------------------------------------------------------------------- 1 |
5 |
6 | 7 | 8 | 13 |
14 | 20 |
21 | 23 | 37 | 38 |
9 |
10 | 11 |
12 |
22 | 24 |
25 |
26 |
32 | 33 |
34 |
35 |
36 |
39 |
40 |
-------------------------------------------------------------------------------- /src/app/sample-categories/sample-categories-panel.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { AfterViewInit, Component } from '@angular/core'; 7 | import { ISampleQueryCategory } from '../base'; 8 | import { SampleCategories, saveCategoryDisplayState } from '../getting-started-queries'; 9 | import { GraphExplorerComponent } from '../GraphExplorerComponent'; 10 | 11 | declare let mwfAutoInit: any; 12 | 13 | @Component({ 14 | selector: 'sample-categories-panel', 15 | styleUrls: ['./sample-categories-panel.component.css'], 16 | templateUrl: './sample-categories-panel.component.html', 17 | }) 18 | export class SampleCategoriesPanelComponent extends GraphExplorerComponent implements AfterViewInit { 19 | public categories: ISampleQueryCategory[] = SampleCategories; 20 | 21 | public ngAfterViewInit(): void { 22 | mwfAutoInit.ComponentFactory.create([{ 23 | component: mwfAutoInit.Toggle, 24 | }]); 25 | } 26 | 27 | public toggleCategory(category: ISampleQueryCategory) { 28 | category.enabled = !category.enabled; 29 | saveCategoryDisplayState(category); 30 | } 31 | 32 | /* 33 | Creates an identifier from the category title by removing whitespace. We need this to 34 | have the checkbox elements labelled by the containing category row so that screen readers 35 | correctly state the purpose of the checkbox. 36 | */ 37 | public getId(title: string): string { 38 | return this.getStr(title).replace(/\s+/, ''); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/telemetry/telemetry.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationInsights, SeverityLevel } from '@microsoft/applicationinsights-web'; 2 | 3 | interface ITelemetry { 4 | initialize(): void; 5 | trackEvent(eventName: string, payload: any): void; 6 | trackException(error: Error, severityLevel: any): void; 7 | } 8 | 9 | class Telemetry implements ITelemetry { 10 | private appInsights: ApplicationInsights; 11 | private instrumentationKey: string; 12 | 13 | constructor() { 14 | this.instrumentationKey = (window as any).InstrumentationKey || ''; 15 | const { mscc } = (window as any); 16 | 17 | const config = { 18 | instrumentationKey: this.instrumentationKey, 19 | disableExceptionTracking: true, 20 | disableTelemetry: this.instrumentationKey ? false : true, 21 | }; 22 | 23 | this.appInsights = new ApplicationInsights({ config }); 24 | } 25 | 26 | public initialize() { 27 | if (this.instrumentationKey) { 28 | this.appInsights.loadAppInsights(); 29 | this.appInsights.addTelemetryInitializer(this.filterFunction); 30 | this.appInsights.trackPageView(); 31 | } 32 | } 33 | 34 | public trackEvent(eventName: string, payload: any) { 35 | this.appInsights.trackEvent({ name: eventName }, payload); 36 | } 37 | 38 | public trackException(error: Error, severityLevel: SeverityLevel) { 39 | this.appInsights.trackException({ error, severityLevel }); 40 | } 41 | 42 | private filterFunction(envelope: any): boolean { 43 | // Identifies the source of telemetry collected 44 | envelope.baseData.name = 'Graph Explorer V3'; 45 | 46 | // Removes access token from uri 47 | const uri = envelope.baseData.uri; 48 | if (uri) { 49 | const startOfFragment = uri.indexOf('#'); 50 | const sanitisedUri = uri.substring(0, startOfFragment); 51 | envelope.baseData.uri = sanitisedUri; 52 | } 53 | 54 | return true; 55 | } 56 | } 57 | 58 | export const telemetry = new Telemetry(); 59 | -------------------------------------------------------------------------------- /src/app/authentication/auth.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { UserAgentApplication } from 'msal'; 7 | import { AppComponent } from '../app.component'; 8 | import { deleteHistoryFromLocalStorage } from '../history/history'; 9 | import { getAccount, getTokenSilent } from './auth.service'; 10 | import { app } from './msal-user-agent'; 11 | 12 | export function localLogout() { 13 | // Anonymous users can only GET 14 | AppComponent.explorerValues.selectedOption = 'GET'; 15 | AppComponent.explorerValues.authentication.user = {}; 16 | AppComponent.explorerValues.authentication.status = 'anonymous'; 17 | deleteHistoryFromLocalStorage(); 18 | sessionStorage.clear(); 19 | } 20 | 21 | export async function checkHasValidAuthToken(userAgentApp: UserAgentApplication) { 22 | const hasAccount = await getAccount(userAgentApp); 23 | const authenticated = isAuthenticated(); 24 | if (!hasAccount && authenticated) { 25 | localLogout(); 26 | } 27 | } 28 | 29 | export function isAuthenticated() { 30 | const status = AppComponent.explorerValues.authentication.status ; 31 | if (status && status !== 'anonymous') { 32 | return true; 33 | } 34 | AppComponent.explorerValues.authentication.status = 'anonymous'; 35 | return false; 36 | } 37 | 38 | // tslint:disable-next-line:only-arrow-functions 39 | (window as any).tokenPlease = function() { 40 | const scopes = AppComponent.Options.DefaultUserScopes; 41 | 42 | getTokenSilent(app, scopes).then( 43 | (result) => { 44 | // tslint:disable-next-line:no-console 45 | console.log(result.accessToken); 46 | }, 47 | (err) => { 48 | // tslint:disable-next-line:no-console 49 | console.log('Please sign in to get your access token'); 50 | }, 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest"], 3 | "rules": { 4 | "comment-format": [true, "check-uppercase"], 5 | "quotemark": [true, "single"], 6 | "max-line-length": [true, 120], 7 | "indent": [true, "spaces", 2], 8 | "no-var-keyword": true, 9 | "prefer-const": true, 10 | "ban-comma-operator": true, 11 | "no-unused-expression": true, 12 | "no-duplicate-variable": true, 13 | "curly": true, 14 | "class-name": true, 15 | "triple-equals": true, 16 | "object-literal-sort-keys": false, 17 | "no-submodule-imports": false, 18 | "semicolon": [ 19 | true, 20 | "always", 21 | "ignore-bound-class-methods" 22 | ], 23 | "whitespace": [true, 24 | "check-branch", 25 | "check-decl", 26 | "check-operator", 27 | "check-module", 28 | "check-separator", 29 | "check-type", 30 | "check-preblock", 31 | "check-typecast" 32 | ], 33 | "typedef-whitespace": [ 34 | true, 35 | { 36 | "call-signature": "nospace", 37 | "index-signature": "nospace", 38 | "parameter": "nospace", 39 | "property-declaration": "nospace", 40 | "variable-declaration": "nospace" 41 | }, 42 | { 43 | "call-signature": "onespace", 44 | "index-signature": "onespace", 45 | "parameter": "onespace", 46 | "property-declaration": "onespace", 47 | "variable-declaration": "onespace" 48 | } 49 | ], 50 | "ban-types": { 51 | "options": [ 52 | ["Object", "Avoid using the `Object` type. Did you mean `object`?"], 53 | ["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"], 54 | ["Number", "Avoid using the `Number` type. Did you mean `number`?"], 55 | ["String", "Avoid using the `String` type. Did you mean `string`?"] 56 | ] 57 | } 58 | }, 59 | "linterOptions": { 60 | "exclude": [ 61 | "src/app/loc_strings.ts", 62 | "src/app/generate-queries/gen-queries.ts", 63 | "src/node_modules/**" 64 | ] 65 | } 66 | } -------------------------------------------------------------------------------- /src/app/api-explorer-jsviewer.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { commonAceSetup, getAceEditorFromElId, getJsonViewer } from './api-explorer-jseditor'; 7 | import { GraphExplorerComponent } from './GraphExplorerComponent'; 8 | 9 | /** 10 | * Gets the localized aria-label for a response viewer textarea. 11 | * @param ariaLabel The aria-label value to add to a textarea element. 12 | */ 13 | function getTextAreaAriaLabel(ariaLabel: string): string { 14 | const g = new GraphExplorerComponent(); 15 | return g.getStr(ariaLabel); 16 | } 17 | 18 | export function initializeJsonViewer() { 19 | 20 | const jsonViewer = getJsonViewer(); 21 | const name = getTextAreaAriaLabel('Response body'); 22 | 23 | jsonViewer.textInput.getElement().setAttribute('name', name); 24 | 25 | commonAceSetup(jsonViewer); 26 | jsonViewer.getSession().setMode('ace/mode/javascript'); 27 | 28 | jsonViewer.setOptions({ 29 | readOnly: true, 30 | highlightActiveLine: false, 31 | highlightGutterLine: false, 32 | }); 33 | 34 | jsonViewer.getSession().setUseWorker(false); 35 | jsonViewer.renderer.$cursorLayer.element.style.opacity = 0; 36 | 37 | } 38 | 39 | export function initializeResponseHeadersViewer() { 40 | const jsonViewer = getAceEditorFromElId('response-header-viewer'); 41 | const name = getTextAreaAriaLabel('Response headers viewer'); 42 | 43 | jsonViewer.textInput.getElement().setAttribute('name', name); 44 | 45 | commonAceSetup(jsonViewer); 46 | 47 | jsonViewer.setOptions({ 48 | readOnly: true, 49 | highlightActiveLine: false, 50 | highlightGutterLine: false, 51 | }); 52 | 53 | jsonViewer.getSession().setUseWorker(false); 54 | jsonViewer.renderer.$cursorLayer.element.style.opacity = 0; 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/app/getting-started-queries.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { ISampleQueryCategory } from './base'; 7 | import { SampleQueries } from './generate-queries/gen-queries'; 8 | 9 | export function getLocalStorageDisplayKey(category: ISampleQueryCategory) { 10 | return `CATEGORY_DISPLAY_${category.title}`; 11 | } 12 | 13 | export function saveCategoryDisplayState(category: ISampleQueryCategory) { 14 | localStorage.setItem(getLocalStorageDisplayKey(category), JSON.stringify(category.enabled)); 15 | } 16 | 17 | export function getCategoryDisplayState(category: ISampleQueryCategory) { 18 | const possibleStatus = localStorage.getItem(getLocalStorageDisplayKey(category)); 19 | 20 | if (possibleStatus !== undefined) { 21 | return JSON.parse(possibleStatus); 22 | } 23 | 24 | return null; 25 | } 26 | 27 | interface IQueryCategoriesMap { 28 | [CategoryTitle: string]: ISampleQueryCategory; 29 | } 30 | 31 | const categories: IQueryCategoriesMap = {}; 32 | 33 | for (const query of SampleQueries) { 34 | 35 | // Insert query into category (create or add to) 36 | if (query.category in categories) { 37 | categories[query.category].queries.push(query); 38 | } else { 39 | categories[query.category] = { 40 | enabled: query.category === 'Getting Started', 41 | queries: [query], 42 | title: query.category, 43 | }; 44 | } 45 | } 46 | 47 | export let SampleCategories: ISampleQueryCategory[] = []; 48 | 49 | for (const categoryTitle in categories) { // tslint:disable-line 50 | const category = categories[categoryTitle]; 51 | const displayCategory = getCategoryDisplayState(category); 52 | 53 | if (displayCategory !== null) { 54 | category.enabled = displayCategory; 55 | } 56 | SampleCategories.push(category); 57 | } 58 | -------------------------------------------------------------------------------- /src/app/authentication/authentication.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{getStr('Using demo tenant')}}
3 |
{{getStr('To access your own data:')}} 4 |
5 |
6 | 11 |
12 |
13 |
14 |
16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 | {{authInfo.user.displayName}}
31 |
32 | {{authInfo.user.emailAddress}}
33 |
34 |
35 | {{getStr('modify permissions')}} 37 | {{getStr('sign out')}} 38 |
39 | -------------------------------------------------------------------------------- /src/index-aot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/app/request-editors.component.html: -------------------------------------------------------------------------------- 1 |
2 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 43 | 47 | 48 |
{{getStr('Key')}}{{getStr('Value')}}
37 | 39 | 41 | 42 | 45 | 46 |
49 |
50 |
51 |
-------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/app/request-editors.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { AfterViewInit, Component } from '@angular/core'; 7 | 8 | import { getRequestBodyEditor, initializeAceEditor } from './api-explorer-jseditor'; 9 | import { IGraphRequestHeader } from './base'; 10 | import { GraphExplorerComponent } from './GraphExplorerComponent'; 11 | 12 | @Component({ 13 | selector: 'request-editors', 14 | styleUrls: ['./request-editors.component.css'], 15 | templateUrl: './request-editors.component.html', 16 | }) 17 | export class RequestEditorsComponent extends GraphExplorerComponent implements AfterViewInit { 18 | public ngAfterViewInit(): void { 19 | this.addEmptyHeader(); 20 | this.initPostBodyEditor(); 21 | } 22 | 23 | public initPostBodyEditor() { 24 | const postBodyEditor = getRequestBodyEditor(); 25 | initializeAceEditor(postBodyEditor, this.getStr('Request body editor')); 26 | } 27 | 28 | public isLastHeader(header: IGraphRequestHeader) { 29 | return header === this.getLastHeader(); 30 | } 31 | 32 | public getPlaceholder(header: IGraphRequestHeader) { 33 | if (this.getLastHeader() === header) { 34 | return this.getStr('Enter new header'); 35 | } 36 | } 37 | 38 | /** 39 | * Check the keyboard input and remove the header if Enter/Return 40 | * is selected when the button is the active element. 41 | * @param e The event arguments. 42 | * @param header The header key to remove from the request UI. 43 | */ 44 | public removeHeaderKeyDown(e: any, header: IGraphRequestHeader) { 45 | // Enter/return 46 | if (e.keyCode === 13) { 47 | this.removeHeader(header); 48 | } 49 | } 50 | 51 | /** 52 | * Remove a header from the request UI. 53 | * @param header The header key to remove from the request UI. 54 | */ 55 | public removeHeader(header: IGraphRequestHeader) { 56 | const idx = this.explorerValues.headers.indexOf(header); 57 | 58 | if (idx !== -1) { 59 | this.explorerValues.headers.splice(idx, 1); 60 | } else { 61 | throw new Error('Can\'t remove header'); 62 | } 63 | } 64 | 65 | public createNewHeaderField() { 66 | if (this.getLastHeader().name !== '') { 67 | this.addEmptyHeader(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { NgModule } from '@angular/core'; 7 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 8 | import { HttpModule } from '@angular/http'; 9 | import { BrowserModule } from '@angular/platform-browser'; 10 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 11 | import { AppComponent } from './app.component'; 12 | import { AriaSelectedMSPivotLinkDirective } from './aria-selected.directive'; 13 | import { AuthenticationComponent } from './authentication/authentication.component'; 14 | import { LogoutComponent } from './authentication/logout.component'; 15 | import { CanaryComponent } from './canary/canary.component'; 16 | import { GenericDialogComponent } from './generic-message-dialog.component'; 17 | import { HistoryPanelComponent } from './history/history-panel.component'; 18 | import { HistoryRowComponent } from './history/history-query.component'; 19 | import { MainColumnComponent } from './main-column/main-column.component'; 20 | import { MethodBadgeComponent } from './method-badge.component'; 21 | import { BannerComponent } from './opt-in-out-banner/banner.component'; 22 | import { QueryRowComponent } from './queryrow/queryrow.component'; 23 | import { RequestEditorsComponent } from './request-editors.component'; 24 | import { ResponseStatusBarComponent } from './response-status-bar/response-status-bar.component'; 25 | import { SampleCategoriesPanelComponent } from './sample-categories/sample-categories-panel.component'; 26 | import { ScopesDialogComponent } from './scopes-dialog/scopes-dialog.component'; 27 | import { ShareLinkBtnComponent } from './share-link/share-link-btn.component'; 28 | import { SidebarComponent } from './sidebar/sidebar.component'; 29 | 30 | @NgModule({ 31 | imports: [BrowserModule, FormsModule, ReactiveFormsModule, HttpModule, BrowserAnimationsModule], 32 | declarations: [AppComponent, AriaSelectedMSPivotLinkDirective, ResponseStatusBarComponent, 33 | AuthenticationComponent, SidebarComponent, QueryRowComponent, MainColumnComponent, HistoryRowComponent, 34 | HistoryPanelComponent, MethodBadgeComponent, SampleCategoriesPanelComponent, RequestEditorsComponent, 35 | ShareLinkBtnComponent, ScopesDialogComponent, GenericDialogComponent, LogoutComponent, BannerComponent, 36 | CanaryComponent], 37 | bootstrap: [AppComponent], 38 | }) 39 | export class AppModule { } 40 | -------------------------------------------------------------------------------- /src/app/history/history-query.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; 7 | import { ISampleQuery } from '../base'; 8 | import { QueryRunnerService } from '../query-runner.service'; 9 | import { QueryRowComponent } from '../queryrow/queryrow.component'; 10 | 11 | declare let moment: any; 12 | 13 | @Component({ 14 | selector: 'history-query-row', 15 | providers: [QueryRunnerService], 16 | template: ` 17 | 18 |
19 | {{query.statusCode}} 20 | {{query.relativeDate}} 21 | {{query.duration}} {{getStr('milliseconds')}} 22 |
23 |
24 | `, 25 | styles: [` 26 | .status-code.success { 27 | color: #05f505; 28 | } 29 | 30 | .status-code.error { 31 | color: #f7688d; 32 | } 33 | 34 | .status-code { 35 | float: left; 36 | } 37 | 38 | .date { 39 | font-weight: normal; 40 | color: #a0a0a0; 41 | } 42 | 43 | .duration { 44 | float: right; 45 | color: #a0a0a0; 46 | } 47 | 48 | .history-row-2 { 49 | text-align: center; 50 | margin-left: 70px; 51 | } 52 | `], 53 | }) 54 | export class HistoryRowComponent extends QueryRowComponent implements OnInit { 55 | public successClass: string; 56 | public updateMomentRef: NodeJS.Timer; 57 | @Input() public query: ISampleQuery; 58 | 59 | constructor(private changeDetectorRef: ChangeDetectorRef, public queryRunnerService: QueryRunnerService) { 60 | super(queryRunnerService); 61 | } 62 | 63 | public ngOnInit(): void { 64 | this.updateMomentRef = setInterval(() => { 65 | this.setRelativeDate(); 66 | }, 1000 * 8); 67 | this.setRelativeDate(); 68 | this.successClass = this.query.statusCode >= 200 && this.query.statusCode < 300 ? 'success' : 'error'; 69 | } 70 | 71 | public setRelativeDate() { 72 | this.query.relativeDate = moment(this.query.requestSentAt).fromNow(); 73 | this.changeDetectorRef.detectChanges(); 74 | } 75 | 76 | public ngOnDestroy() { 77 | clearInterval(this.updateMomentRef); 78 | this.changeDetectorRef.detach(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/history/history-panel.component.html: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 |

{{getStr('History')}}

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 27 | 28 | 29 | 32 | 34 | 35 | 38 | 39 | 40 |
{{getStr('Method')}}{{getStr('Query')}}{{getStr('Date')}}{{getStr('Status Code')}}{{getStr('Duration')}}{{getStr("Action")}}
25 | 26 | {{getQueryText(query)}}{{query.relativeDate}} 30 | {{query.statusCode}} 31 | {{query.duration}} 33 | {{getStr('milliseconds')}}{{getStr("Export")}} 36 | 37 |
41 |
42 | 46 |
47 | 48 |
49 |
50 |
51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microsoft-graph-explorer", 3 | "version": "1.9.0", 4 | "description": "Microsoft Graph Explorer", 5 | "scripts": { 6 | "build": "tsc -p src/", 7 | "build:watch": "npm run build -- -w", 8 | "build:aot": "ngc -p src/tsconfig-aot.json", 9 | "build:prod": "npm run build:aot && npm run rollup && node versioned-build", 10 | "start": "concurrently \"npm run build:watch\" \"npm run serve\" \"node file-utility\"", 11 | "serve": "lite-server -c=bs-config.json", 12 | "test": "npm run build && karma start karma.conf.js --single-run --browsers ChromeHeadless", 13 | "test:watch": "concurrently \"npm run build:watch\" \"karma start karma.conf.js\"", 14 | "import:samples": "node util-scripts\\query-importer.js", 15 | "import:loc-strings": "node util-scripts\\bundle-loc-files.js", 16 | "rollup": "rollup -c rollup-config.js", 17 | "lint": "tslint --project src/tsconfig.json -c tslint.json" 18 | }, 19 | "author": "", 20 | "devDependencies": { 21 | "@angular/compiler-cli": "^4.1.3", 22 | "@types/jasmine": "=2.5.47", 23 | "@types/jquery": "^2.0.40", 24 | "chokidar": "^2.0.4", 25 | "concurrently": "^3.2.0", 26 | "fast-csv": "2.4.0", 27 | "husky": "^1.1.2", 28 | "jasmine-core": "2.6.1", 29 | "jquery": "^3.4.0", 30 | "lite-server": "^2.3.0", 31 | "rollup": "^0.41.6", 32 | "rollup-plugin-commonjs": "^8.0.2", 33 | "rollup-plugin-node-resolve": "^3.0.0", 34 | "rollup-plugin-uglify": "^2.0.1", 35 | "tslint": "^5.11.0", 36 | "typescript": "~2.3.0" 37 | }, 38 | "dependencies": { 39 | "@angular/animations": "^4.1.3", 40 | "@angular/common": "^4.1.3", 41 | "@angular/compiler": "^4.1.3", 42 | "@angular/core": "^4.1.3", 43 | "@angular/forms": "^4.1.3", 44 | "@angular/http": "^4.1.3", 45 | "@angular/platform-browser": "^4.1.3", 46 | "@angular/platform-browser-dynamic": "^4.1.3", 47 | "@angular/platform-server": "^4.1.3", 48 | "@angular/router": "^4.1.3", 49 | "@microsoft/applicationinsights-web": "^2.4.1", 50 | "angular-in-memory-web-api": "^0.3.0", 51 | "core-js": "^2.4.1", 52 | "guid-typescript": "^1.0.9", 53 | "har-schema": "^2.0.0", 54 | "karma": "^1.7.0", 55 | "karma-chrome-launcher": "^2.1.0", 56 | "karma-cli": "^1.0.1", 57 | "karma-jasmine": "^1.1.0", 58 | "karma-jasmine-html-reporter": "^0.2.2", 59 | "moment": "^2.21.0", 60 | "msal": "^1.1.3", 61 | "npm": "^6.4.1", 62 | "rxjs": "^5.4.0", 63 | "systemjs": "^0.19.27", 64 | "zone.js": "^0.8.5" 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "git+https://github.com/microsoftgraph/microsoft-graph-explorer.git" 69 | }, 70 | "bugs": { 71 | "url": "https://github.com/microsoftgraph/microsoft-graph-explorer/issues" 72 | }, 73 | "homepage": "https://github.com/microsoftgraph/microsoft-graph-explorer#readme" 74 | } 75 | -------------------------------------------------------------------------------- /src/app/sidebar/sidebar.component.css: -------------------------------------------------------------------------------- 1 | #explorer-sidebar { 2 | background: #2F2F2F !important; 3 | min-height: 1024px; 4 | padding: 0px; 5 | color: white; 6 | font-family: "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif; 7 | } 8 | 9 | #explorer-sidebar .c-hyperlink { 10 | color: #00bcf2; 11 | } 12 | 13 | #getting-started-svg { 14 | display: inline-block; 15 | width: 29px; 16 | height: 29px; 17 | margin: -2px 4px 2px -4px; 18 | } 19 | 20 | a#show-full-history, a#manage-categories { 21 | text-align: right; 22 | padding-right: 16px; 23 | width: 100%; 24 | display: block; 25 | } 26 | 27 | span#explorer-title { 28 | margin-left: 10px; 29 | margin-top: 14px; 30 | } 31 | 32 | .c-drawer { 33 | padding-bottom: 5px; 34 | background: #2f2f2f; 35 | } 36 | 37 | #explorer-sidebar .panel-header { 38 | font-family: "Segoe UI","wf_segoe-ui_normal","Arial",sans-serif; 39 | display: inline-block; 40 | padding: 0px; 41 | padding-left: 6px; 42 | font-weight: 100; 43 | color: white; 44 | } 45 | 46 | #explorer-sidebar .panel-content { 47 | padding-left: 28px; 48 | font-size: 13px; 49 | } 50 | 51 | #explorer-sidebar .panel-header i.ms-Icon{ 52 | margin-right: 10px; 53 | } 54 | 55 | /* Remove drawer carrot on auth */ 56 | #auth-drawer-button:after{ 57 | content:none; 58 | } 59 | 60 | button#auth-drawer-button { 61 | background: #2f2f2f !important; 62 | } 63 | 64 | #auth-drawer-canary-button:after{ 65 | content:none; 66 | } 67 | 68 | button#auth-drawer-canary-button { 69 | background: #2f2f2f !important; 70 | } 71 | 72 | .arrow-left { 73 | border-top: 18px solid transparent; 74 | border-bottom: 18px solid transparent; 75 | border-right: 18px solid white; 76 | position: relative; 77 | right: -10px; 78 | top: 13px; 79 | margin-bottom: -45px; 80 | } 81 | 82 | button.c-glyph { 83 | color: white; 84 | } 85 | 86 | #authDrawer { 87 | min-height: 126px; 88 | } 89 | 90 | .c-hyperlink { 91 | color: #00bcf2; 92 | } 93 | 94 | .category-heading { 95 | font-size: 17px; 96 | font-weight: 300; 97 | padding-bottom: 5px; 98 | display: block; 99 | } 100 | 101 | .sample-category { 102 | margin-bottom: 15px; 103 | } 104 | 105 | 106 | a#show-full-history { 107 | margin-top: 15px; 108 | } 109 | a#show-full-history[hidden] { 110 | display: none; 111 | } 112 | 113 | #sample-query-btn { 114 | background: #2F2F2F; 115 | } 116 | 117 | #sample-history-btn { 118 | background: #2F2F2F; 119 | } 120 | 121 | @media (max-width: 639px) { 122 | #explorer-sidebar { 123 | min-height: inherit; 124 | padding-bottom: 15px; 125 | } 126 | } 127 | 128 | .title-container { 129 | display: flex; 130 | justify-content: space-between; 131 | } 132 | -------------------------------------------------------------------------------- /src/app/queryrow/queryrow.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { Component, Input } from '@angular/core'; 7 | import { getShortQueryText } from './../ApiCallDisplayHelpers'; 8 | import { AppComponent } from './../app.component'; 9 | import { ISampleQuery } from './../base'; 10 | import { GraphExplorerComponent } from './../GraphExplorerComponent'; 11 | import { QueryRunnerService } from './../query-runner.service'; 12 | 13 | @Component({ 14 | selector: 'query-row', 15 | templateUrl: './queryrow.component.html', 16 | styleUrls: ['./queryrow.component.css'], 17 | providers: [QueryRunnerService], 18 | }) 19 | export class QueryRowComponent extends GraphExplorerComponent { 20 | 21 | @Input() public category: string; 22 | @Input() public query: ISampleQuery; 23 | 24 | constructor(public queryRunnerService: QueryRunnerService) { 25 | super(); 26 | } 27 | 28 | public queryKeyDown(event) { 29 | if (event.keyCode === 13) { 30 | this.loadQueryIntoEditor(this.query); 31 | } 32 | } 33 | 34 | public getTitle() { 35 | return this.getAriaLabelForSampleRow(); // This.getQueryText() + " | " + this.query.requestUrl; 36 | } 37 | 38 | public getQueryText() { 39 | return getShortQueryText(this.query); 40 | } 41 | 42 | /** 43 | * @returns The identifier of query button element. 44 | */ 45 | public getQueryButtonId(): string { 46 | return (this.query.method + ' ' + this.getQueryText()).replace(/\s+/g, '-').toLowerCase(); 47 | } 48 | 49 | /** 50 | * @returns The value for the aria-labelledby element that is used by AT. 51 | */ 52 | public getAriaLabelledBy(): string { 53 | return this.category + ' ' + this.getQueryButtonId(); 54 | } 55 | 56 | public getAriaLabelForSampleRow() { 57 | return this.query.method + ' ' + this.getQueryText() + ' sample request'; 58 | } 59 | 60 | public handleQueryClick() { 61 | this.loadQueryIntoEditor(this.query); 62 | 63 | if (this.query.method === 'GET') { 64 | if (!this.query.tip || !this.isAuthenticated()) { 65 | this.queryRunnerService.executeExplorerQuery(true); 66 | } else if (this.query.tip) { 67 | this.displayTipMessage(); 68 | } 69 | } else if (this.query.tip && this.isAuthenticated()) { 70 | this.displayTipMessage(); 71 | } 72 | } 73 | 74 | public displayTipMessage() { 75 | AppComponent.messageBarContent = { 76 | backgroundClass: 'ms-MessageBar--warning', 77 | icon: 'ms-Icon--Info', 78 | text: this.query.tip, 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /assets/images/rocket1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 19 | 27 | 30 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/systemjs.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System configuration for Angular samples 3 | * Adjust as necessary for your application needs. 4 | */ 5 | (function () { 6 | System.config({ 7 | paths: { 8 | // paths serve as alias 9 | 'npm:': 'node_modules/' 10 | }, 11 | // map tells the System loader where to look for things 12 | map: { 13 | // our app is within the app folder 14 | 'app': 'compiler-output/app', 15 | 16 | // angular bundles 17 | '@angular/core': 'npm:@angular/core/bundles/core.umd.js', 18 | '@angular/common': 'npm:@angular/common/bundles/common.umd.js', 19 | '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js', 20 | '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js', 21 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 22 | '@angular/http': 'npm:@angular/http/bundles/http.umd.js', 23 | '@angular/router': 'npm:@angular/router/bundles/router.umd.js', 24 | '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', 25 | '@angular/animations': 'npm:@angular/animations/bundles/animations.umd.js', 26 | '@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js', 27 | '@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js', 28 | 29 | // other libraries 30 | 'rxjs': 'npm:rxjs', 31 | 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js', 32 | 'jwt-decode': 'npm:jwt-decode/build/jwt-decode.min.js', 33 | 'msal': 'npm:msal/dist/msal.js', 34 | 'guid-typescript': 'npm:guid-typescript/dist/guid.js', 35 | 36 | '@microsoft/applicationinsights-core-js': 'npm:@microsoft/applicationinsights-core-js/dist/applicationinsights-core-js.min.js', 37 | '@microsoft/applicationinsights-analytics-js': 'npm:@microsoft/applicationinsights-analytics-js/dist/applicationinsights-analytics-js.js', 38 | '@microsoft/applicationinsights-channel-js': 'npm:@microsoft/applicationinsights-channel-js/dist/applicationinsights-channel-js.min.js', 39 | '@microsoft/applicationinsights-common': 'npm:@microsoft/applicationinsights-common/dist/applicationinsights-common.min.js', 40 | '@microsoft/applicationinsights-dependencies-js': 'npm@microsoft/applicationinsights-dependencies-js/dist/applicationinsights-dependencies-js/dist/applicationinsights-dependencies-js.min.js', 41 | '@microsoft/applicationinsights-properties-js': 'npm@microsoft/applicationinsights-properties-js/dist/applicationinsights-properties-js.min.js', 42 | '@microsoft/applicationinsights-web': 'npm:@microsoft/applicationinsights-web/dist/applicationinsights-web.min.js' 43 | }, 44 | 45 | // packages tells the System loader how to load when no filename and/or no extension 46 | packages: { 47 | app: { 48 | defaultExtension: 'js', 49 | meta: { 50 | './*.js': { 51 | loader: 'systemjs-angular-loader.js' 52 | } 53 | } 54 | }, 55 | rxjs: { 56 | defaultExtension: 'js' 57 | } 58 | } 59 | }); 60 | })(this); 61 | -------------------------------------------------------------------------------- /src/custom.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. 3 | // ------------------------------------------------------------------------------*/ 4 | .ace_editor { 5 | box-shadow: none !important; 6 | position: relative; 7 | } 8 | 9 | #response-viewer-labels { 10 | margin-top: 10px; 11 | } 12 | 13 | .ms-Pivot-link:focus { 14 | outline: none; 15 | } 16 | 17 | #jsonViewer, #response-header-viewer { 18 | height: 50vh; 19 | border: 1px solid #ccc; 20 | } 21 | 22 | .ace_link_marker { 23 | position: absolute; 24 | border-bottom: 1px solid blue; 25 | } 26 | 27 | .ace-tm .ace_invalid { 28 | color: inherit; 29 | background-color: inherit; 30 | } 31 | 32 | #graph-version-select #-Other { 33 | display: none; 34 | } 35 | 36 | #graph-version-select button:after { 37 | color: black; 38 | font-size: 12px; 39 | } 40 | 41 | api-explorer button { 42 | min-width: initial; 43 | } 44 | 45 | .ms-Spinner-circle { 46 | background-color: white !important; 47 | } 48 | 49 | api-explorer .c-select-menu>a:after, api-explorer .c-select-menu>button:after { 50 | position: absolute; 51 | font-size: 8px; 52 | color: rgba(0,0,0,.8); 53 | font-weight: 700; 54 | background: white; 55 | height: 100%; 56 | padding-right: 5px; 57 | right: 0px; 58 | top: 0px; 59 | padding-top: 14px; 60 | padding-left: 5px; 61 | } 62 | 63 | #httpMethodSelect button { 64 | color: white; 65 | width: 85px; 66 | text-align: center; 67 | } 68 | 69 | #httpMethodSelect button:after { 70 | font-size: 12px; 71 | color: black; 72 | } 73 | 74 | #httpMethodSelect.GET .c-select-menu.f-border button { 75 | background-color: #000fdf; 76 | } 77 | 78 | #httpMethodSelect.POST .c-select-menu.f-border button { 79 | background-color: #008412; 80 | } 81 | 82 | #httpMethodSelect.PATCH .c-select-menu.f-border button { 83 | background-color: #be8b00; 84 | } 85 | 86 | #httpMethodSelect.DELETE .c-select-menu.f-border button { 87 | background-color: #a10000; 88 | } 89 | 90 | #httpMethodSelect.PUT .c-select-menu.f-border button { 91 | background-color: #5C005C; 92 | } 93 | 94 | .m-auto-suggest .c-menu-item strong { 95 | font-weight: normal; 96 | } 97 | 98 | 99 | /*override mwf*/ 100 | .fixed-with-mwf-menu ul.c-menu { 101 | width: 100px !important; 102 | } 103 | 104 | /* frontdoor */ 105 | .ms-PanelHost { 106 | z-index: 3000002; 107 | } 108 | 109 | #deltaUi { 110 | display: none; 111 | } 112 | 113 | .ms-Overlay.is-visible { 114 | z-index: 990; 115 | } 116 | 117 | .c-menu-item { 118 | margin-bottom: 0px; 119 | } 120 | 121 | .ms-Panel.is-open { 122 | z-index: 999; 123 | } 124 | 125 | .c-search input[type=search] { 126 | padding: 7px; 127 | max-height: 32px; 128 | padding-right: 0px; 129 | } 130 | 131 | .ms-Dialog { 132 | z-index: 999; 133 | } 134 | 135 | *:focus { 136 | outline: 2px solid #0877AA; 137 | } 138 | 139 | .link { 140 | color: #006cd8 !important; 141 | } 142 | -------------------------------------------------------------------------------- /src/app/history/har/IHarFormat.ts: -------------------------------------------------------------------------------- 1 | interface IHarLog { 2 | version: string; 3 | creator: IHarCreator; 4 | browser?: IHarBrowser; 5 | entries: IHarEntries[]; 6 | comment?: string; 7 | } 8 | 9 | interface IHarCreator { 10 | name: string; 11 | version: string; 12 | comment?: string; 13 | } 14 | 15 | interface IHarBrowser { 16 | name: string; 17 | version: string; 18 | comment?: string; 19 | } 20 | 21 | interface IHarEntries { 22 | pageref?: string; 23 | startedDateTime: string; 24 | time: number; 25 | request: IHarRequest; 26 | response: IHarResponse; 27 | cache: IHarCache; 28 | timings: IHarTimings; 29 | serverIpAddress?: string; 30 | connection?: string; 31 | comment?: string; 32 | } 33 | 34 | interface IHarRequest { 35 | method: string; 36 | url: string; 37 | httpVersion: string; 38 | cookies: IHarCookies[]; 39 | headers: IHarHeaders[]; 40 | queryString: IHarQueryString[]; 41 | postData?: IHarPostData; 42 | headersSize: number; 43 | bodySize: number; 44 | comment?: string; 45 | } 46 | 47 | interface IHarResponse { 48 | status: number; 49 | statusText: string; 50 | httpVersion: string; 51 | cookies: IHarCookies[]; 52 | headers: IHarHeaders[]; 53 | content: IHarContent; 54 | redirectURL: string; 55 | headersSize: number; 56 | bodySize: number; 57 | comment?: string; 58 | } 59 | 60 | interface IHarCookies { 61 | name: string; 62 | value: string; 63 | path?: string; 64 | domain?: string; 65 | expires?: string; 66 | httpOnly?: boolean; 67 | secure?: boolean; 68 | comment?: string; 69 | } 70 | 71 | interface IHarHeaders { 72 | name: string; 73 | value: string; 74 | comment?: string; 75 | } 76 | 77 | interface IHarQueryString { 78 | name: string; 79 | value: string; 80 | comment?: string; 81 | } 82 | 83 | interface IHarPostData { 84 | mimeType: string; 85 | params?: IHarParams; 86 | text: string; 87 | comment?: string; 88 | } 89 | 90 | interface IHarParams { 91 | name: string; 92 | value?: string; 93 | fileName?: string; 94 | contentType?: string; 95 | comment?: string; 96 | } 97 | 98 | interface IHarContent { 99 | size: number; 100 | compression?: number; 101 | mimeType: string; 102 | text?: string; 103 | encoding?: string; 104 | comment?: string; 105 | } 106 | 107 | interface IHarCache { 108 | beforeRequest?: object; 109 | afterRequest?: object; 110 | comment?: string; 111 | } 112 | 113 | interface IHarTimings { 114 | blocked?: number; 115 | dns?: number; 116 | connect?: number; 117 | send: number; 118 | wait: number; 119 | receive: number; 120 | ssl?: number; 121 | comment?: string; 122 | } 123 | 124 | export interface IHarFormat { 125 | log: IHarLog; 126 | } 127 | 128 | export interface IHarPayload { 129 | startedDateTime: string; 130 | time: number; 131 | method: string; 132 | url: string; 133 | httpVersion: string; 134 | cookies: IHarCookies[]; 135 | request: { 136 | headers: IHarHeaders[]; 137 | }; 138 | response: { 139 | headers: IHarHeaders[]; 140 | }; 141 | queryString: IHarQueryString[]; 142 | postData?: IHarPostData; 143 | status: number; 144 | statusText: string; 145 | content: IHarContent; 146 | sendTime: number; 147 | waitTime: number; 148 | receiveTime: number; 149 | } 150 | -------------------------------------------------------------------------------- /src/app/authentication/auth.service.ts: -------------------------------------------------------------------------------- 1 | import * as Msal from 'msal'; 2 | 3 | import { AppComponent } from '../app.component'; 4 | 5 | const loginType = getLoginType(); 6 | 7 | export function logout(userAgentApp: Msal.UserAgentApplication) { 8 | userAgentApp.logout(); 9 | } 10 | 11 | // tslint:disable-next-line: max-line-length 12 | export async function getTokenSilent(userAgentApp: Msal.UserAgentApplication, scopes: string[]): Promise { 13 | return userAgentApp.acquireTokenSilent({ scopes: generateUserScopes(scopes) }); 14 | } 15 | 16 | export async function login(userAgentApp: Msal.UserAgentApplication) { 17 | const loginRequest = { 18 | scopes: generateUserScopes(), 19 | prompt: 'select_account', 20 | }; 21 | if (loginType === 'POPUP') { 22 | try { 23 | const response = await userAgentApp.loginPopup(loginRequest); 24 | return response; 25 | } catch (error) { 26 | throw error; 27 | } 28 | } else if (loginType === 'REDIRECT') { 29 | await userAgentApp.loginRedirect(loginRequest); 30 | } 31 | } 32 | 33 | export async function acquireNewAccessToken(userAgentApp: Msal.UserAgentApplication, scopes: string[] = []) { 34 | const hasScopes = (scopes.length > 0); 35 | let listOfScopes = AppComponent.Options.DefaultUserScopes; 36 | if (hasScopes) { 37 | listOfScopes = scopes; 38 | } 39 | return getTokenSilent(userAgentApp, generateUserScopes(listOfScopes)).catch((error) => { 40 | if (requiresInteraction(error.errorCode)) { 41 | if (loginType === 'POPUP') { 42 | try { 43 | return userAgentApp.acquireTokenPopup({ scopes: generateUserScopes(listOfScopes) }); 44 | } catch (error) { 45 | throw error; 46 | } 47 | } else if (loginType === 'REDIRECT') { 48 | userAgentApp.acquireTokenRedirect({ scopes: generateUserScopes(listOfScopes) }); 49 | } 50 | } 51 | }); 52 | } 53 | 54 | export function getAccount(userAgentApp: Msal.UserAgentApplication) { 55 | return userAgentApp.getAccount(); 56 | } 57 | 58 | export function generateUserScopes(userScopes = AppComponent.Options.DefaultUserScopes) { 59 | const graphMode = JSON.parse(localStorage.getItem('GRAPH_MODE')); 60 | if (graphMode === null) { 61 | return userScopes; 62 | } 63 | const graphUrl = localStorage.getItem('GRAPH_URL'); 64 | const reducedScopes = userScopes.reduce((newScopes, scope) => { 65 | if (scope === 'openid' || scope === 'profile') { 66 | return newScopes += scope + ' '; 67 | } 68 | return newScopes += graphUrl + '/' + scope + ' '; 69 | }, ''); 70 | 71 | const scopes = reducedScopes.split(' ').filter((scope) => { 72 | return scope !== ''; 73 | }); 74 | return scopes; 75 | } 76 | 77 | export function requiresInteraction(errorCode) { 78 | if (!errorCode || !errorCode.length) { 79 | return false; 80 | } 81 | return errorCode === 'consent_required' || 82 | errorCode === 'interaction_required' || 83 | errorCode === 'login_required' || 84 | errorCode === 'token_renewal_error'; 85 | } 86 | 87 | export function getLoginType() { 88 | /** 89 | * Always redirects because of transient issues caused by showing a pop up. Graph Explorer 90 | * loses hold of the iframe Pop Up 91 | */ 92 | return 'REDIRECT'; 93 | } 94 | -------------------------------------------------------------------------------- /src/app/sidebar/sidebar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | {{getStr('Graph Explorer')}} 6 | 7 |
8 |
9 | 13 |
14 | 15 |
16 |
17 |
18 | 23 |
24 | 25 |
26 |
27 | 28 |
29 | 35 |
36 |
37 |
38 |
39 | {{getStr(category.title)}} 41 |
42 | 44 |
45 |
46 | 48 | {{getStr('show more samples')}} 49 |
50 |
51 |
52 | 56 |
57 | 58 | {{getStr('Show More')}} 61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /src/app/history/history-panel.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; 6 | 7 | import { getShortQueryText } from '../ApiCallDisplayHelpers'; 8 | import { AppComponent } from '../app.component'; 9 | import { IGraphApiCall } from '../base'; 10 | import { GraphExplorerComponent } from '../GraphExplorerComponent'; 11 | import { QueryRunnerService } from '../query-runner.service'; 12 | import { saveHistoryToLocalStorage } from './history'; 13 | 14 | declare let moment: any; 15 | 16 | @Component({ 17 | selector: 'history-panel', 18 | styleUrls: ['./history-panel.component.css'], 19 | templateUrl: './history-panel.component.html', 20 | providers: [QueryRunnerService], 21 | }) 22 | export class HistoryPanelComponent extends GraphExplorerComponent implements OnInit { 23 | constructor(private changeDetectorRef: ChangeDetectorRef, private queryRunnerService: QueryRunnerService) { 24 | super(); 25 | } 26 | 27 | public ngOnInit(): void { 28 | setInterval(() => { 29 | for (const historyRecord of AppComponent.requestHistory) { 30 | historyRecord.relativeDate = moment(historyRecord.requestSentAt).fromNow(); 31 | } 32 | this.changeDetectorRef.detectChanges(); 33 | }, 5000); 34 | } 35 | 36 | public closeHistoryPanel = () => { 37 | (document.querySelector('#history-panel .ms-Panel-closeButton') as any).click(); 38 | }; 39 | 40 | public getQueryText(query: IGraphApiCall) { 41 | return getShortQueryText(query); 42 | } 43 | 44 | public getSuccessClass(query: IGraphApiCall) { 45 | return query.statusCode >= 200 && query.statusCode < 300 ? 'success' : 'error'; 46 | } 47 | 48 | public removeQueryFromHistory(query: IGraphApiCall) { 49 | AppComponent.removeRequestFromHistory(query); 50 | } 51 | 52 | public clearHistory() { 53 | AppComponent.requestHistory = []; 54 | saveHistoryToLocalStorage(AppComponent.requestHistory); 55 | } 56 | 57 | public handleQueryClick(query: IGraphApiCall) { 58 | if (!this.isAuthenticated() && query.method !== 'GET') { 59 | return; 60 | } 61 | 62 | this.loadQueryIntoEditor(query); 63 | this.closeHistoryPanel(); 64 | 65 | if (query.method === 'GET') { 66 | this.queryRunnerService.executeExplorerQuery(true); 67 | } 68 | } 69 | 70 | public exportQuery(query: IGraphApiCall) { 71 | const blob = new Blob([query.har], { type: 'text/json' }); 72 | 73 | const url = query.requestUrl.substr(8).split('/'); 74 | url.pop(); // Removes leading slash 75 | 76 | const filename = `${url.join('_')}.har`; 77 | 78 | if (window.navigator.msSaveOrOpenBlob) { 79 | window.navigator.msSaveBlob(blob, filename); 80 | } else { 81 | const elem = window.document.createElement('a'); 82 | elem.href = window.URL.createObjectURL(blob); 83 | elem.download = filename; 84 | document.body.appendChild(elem); 85 | elem.click(); 86 | document.body.removeChild(elem); 87 | } 88 | } 89 | 90 | public focusOnCloseBtn() { 91 | (document.querySelector('#history-panel') as any).focus(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/app/share-link/share-link-btn.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { AfterViewInit, Component } from '@angular/core'; 7 | import { AllowedGraphDomains } from '../base'; 8 | import { GraphExplorerComponent } from '../GraphExplorerComponent'; 9 | 10 | declare let fabric: any; 11 | 12 | // Provides a link to share the last query issued from Graph Explorer. 13 | @Component({ 14 | selector: 'share-link-btn', 15 | templateUrl: './share-link-btn.component.html', 16 | styleUrls: ['./share-link-btn.component.css'], 17 | }) 18 | export class ShareLinkBtnComponent extends GraphExplorerComponent implements AfterViewInit { 19 | public ngAfterViewInit(): void { 20 | const DialogElements = document.querySelectorAll('.ms-Dialog'); 21 | const DialogComponents = []; 22 | for (let i = 0; i < DialogElements.length; i++) { 23 | ((() => { 24 | DialogComponents[i] = new fabric.Dialog(DialogElements[i]); 25 | })()); 26 | } 27 | 28 | } 29 | 30 | /** 31 | * Shows the Share Code Dialog if the enter/return key is pressed. 32 | * @param e the event object 33 | */ 34 | public showShareDialogKeyDown(e: any) { 35 | // Enter/return 36 | if (e.keyCode === 13) { 37 | this.showShareDialog(); 38 | } 39 | } 40 | 41 | // Shows the Share Code Dialog. 42 | public showShareDialog() { 43 | const el = document.querySelector('#share-link-dialog'); 44 | const fabricDialog = new fabric.Dialog(el); 45 | fabricDialog.open(); 46 | } 47 | 48 | public getShareLink() { 49 | return this.createShareLink(this.explorerValues.endpointUrl, 50 | this.explorerValues.selectedOption, this.explorerValues.selectedVersion); 51 | } 52 | 53 | public createShareLink(fullRequestUrl, action, version) { 54 | const callComponents = this.getGraphCallComponents(fullRequestUrl); 55 | return window.location.origin 56 | + window.location.pathname 57 | + '?request=' + callComponents.requestUrl 58 | + '&method=' + action 59 | + '&version=' + callComponents.version 60 | + '&GraphUrl=' + callComponents.graphDeploymentUrl; 61 | } 62 | 63 | /** 64 | * Given a URL like https://graph.microsoft.com/v1.0/some/graph/api/call, extract 65 | * https://graph.microsoft.com and some/graph/api/call 66 | */ 67 | public getGraphCallComponents(fullRequestUrl: string): IGraphApiCallUrlComponents { 68 | if (!fullRequestUrl) { 69 | return; 70 | } 71 | 72 | for (const graphDeploymentUrl of AllowedGraphDomains) { 73 | if (fullRequestUrl.startsWith(graphDeploymentUrl)) { 74 | 75 | const apiCall = fullRequestUrl.split(graphDeploymentUrl)[1]; 76 | const requestUrlComponents = apiCall.split('/'); 77 | 78 | return { 79 | graphDeploymentUrl, 80 | version: requestUrlComponents[1], 81 | requestUrl: requestUrlComponents.slice(2, requestUrlComponents.length).join('/'), 82 | }; 83 | } 84 | } 85 | } 86 | } 87 | 88 | interface IGraphApiCallUrlComponents { 89 | graphDeploymentUrl?: string; 90 | requestUrl?: string; 91 | version?: string; 92 | } 93 | -------------------------------------------------------------------------------- /src/app/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ------------------------------------------------------------------------------ 3 | Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 4 | See License in the project root for license information. 5 | ------------------------------------------------------------------------------ 6 | */ 7 | 8 | import { AfterViewInit, Component } from '@angular/core'; 9 | import { ISampleQueryCategory } from '../base'; 10 | import { SampleCategories } from '../getting-started-queries'; 11 | import { GraphExplorerComponent } from '../GraphExplorerComponent'; 12 | 13 | declare let fabric; 14 | 15 | @Component({ 16 | selector: 'sidebar', 17 | templateUrl: './sidebar.component.html', 18 | styleUrls: ['./sidebar.component.css'], 19 | }) 20 | export class SidebarComponent extends GraphExplorerComponent implements AfterViewInit { 21 | public sampleCategoryPanel: Element; 22 | public historyPanel: Element; 23 | public categories: ISampleQueryCategory[] = SampleCategories; 24 | 25 | public openPanel = ''; 26 | 27 | public setOpenPanel(panel: string) { 28 | this.openPanel = panel; 29 | } 30 | 31 | public ngAfterViewInit(): void { 32 | this.historyPanel = document.querySelector('#history-panel'); 33 | this.sampleCategoryPanel = document.querySelector('#sample-categories-panel'); 34 | 35 | $(document).keyup((e) => { 36 | if (e.keyCode === 27) { // Esc 37 | this.closePanels(); 38 | } 39 | }); 40 | 41 | } 42 | 43 | /** 44 | * idifyCategory 45 | * @param categoryTitle The sample category title that will be changed into an element id. 46 | * @returns A sample category title as an ID. 47 | */ 48 | public idifyCategory(categoryTitle: string): string { 49 | return categoryTitle.replace(/\s+/g, '-').toLowerCase(); 50 | } 51 | 52 | public manageCategories() { 53 | // Open sample category panel 54 | new fabric['Panel'](this.sampleCategoryPanel); // tslint:disable-line 55 | 56 | // Set the focus on the first actionable control in the sampleCategoryPanel. 57 | (document.querySelector('#closeSampleCategories') as any).focus(); 58 | 59 | } 60 | 61 | public manageHistory() { 62 | // Open history panel 63 | new fabric['Panel'](this.historyPanel); // tslint:disable-line 64 | (document.querySelector('#close-history-btn') as any).focus(); 65 | } 66 | 67 | public focusOnMoreSamples() { 68 | // Set the focus on the first actionable control in the sampleCategoryPanel. 69 | (document.querySelector('#manage-categories') as any).focus(); 70 | } 71 | 72 | public focusOnShowMore() { 73 | // Set the focus on the first actionable control in the sampleCategoryPanel. 74 | (document.querySelector('#show-full-history') as any).focus(); 75 | } 76 | 77 | public displayCanary() { 78 | if (JSON.parse(localStorage.getItem('GRAPH_MODE')) === null) { 79 | return false; 80 | } 81 | return true; 82 | } 83 | 84 | public closePanels() { 85 | const historyPanel = document.querySelector('#close-history-btn') as any; 86 | const samplesPanel = document.querySelector('#closeSampleCategories') as any; 87 | 88 | if (this.openPanel === 'samples') { 89 | samplesPanel.click(); 90 | this.focusOnMoreSamples(); 91 | } 92 | 93 | if (this.openPanel === 'history') { 94 | historyPanel.click(); 95 | this.focusOnShowMore(); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /karma-test-shim.js: -------------------------------------------------------------------------------- 1 | // /*global jasmine, __karma__, window*/ 2 | Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing. 3 | 4 | // Uncomment to get full stacktrace output. Sometimes helpful, usually not. 5 | // Error.stackTraceLimit = Infinity; // 6 | 7 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000*30; // We are getting random timeouts at 10 sec, increasing to 30 seconds. 8 | 9 | // builtPaths: root paths for output ("built") files 10 | // get from karma.config.js, then prefix with '/base/' (default is 'src/') 11 | var builtPaths = (['src/compiler-output/app/']) 12 | .map(function(p) { return '/base/'+p;}); 13 | 14 | __karma__.loaded = function () { }; 15 | 16 | function isJsFile(path) { 17 | return path.slice(-3) == '.js'; 18 | } 19 | 20 | function isSpecFile(path) { 21 | return /\.spec\.(.*\.)?js$/.test(path); 22 | } 23 | 24 | // Is a "built" file if is JavaScript file in one of the "built" folders 25 | function isBuiltFile(path) { 26 | return isJsFile(path) && 27 | builtPaths.reduce(function(keep, bp) { 28 | return keep || (path.substr(0, bp.length) === bp); 29 | }, false); 30 | } 31 | 32 | var allSpecFiles = Object.keys(window.__karma__.files) 33 | .filter(isSpecFile) 34 | .filter(isBuiltFile); 35 | 36 | 37 | System.config({ 38 | // Base URL for System.js calls. 'base/' is where Karma serves files from. 39 | baseURL: 'base/src', 40 | // Extend usual application package list with test folder 41 | packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } }, 42 | 43 | // Assume pm: is set in `paths` in systemjs.config 44 | // Map the angular testing umd bundles 45 | map: { 46 | '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js', 47 | '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js', 48 | '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js', 49 | '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js', 50 | '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', 51 | '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js', 52 | '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js', 53 | '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js', 54 | 'api-explorer-svc': 'foobar', 55 | }, 56 | }); 57 | 58 | System.import('systemjs.config.js') 59 | .then(importSystemJsExtras) 60 | .then(initTestBed) 61 | .then(initTesting); 62 | 63 | /** Optional SystemJS configuration extras. Keep going w/o it */ 64 | function importSystemJsExtras(){ 65 | return System.import('systemjs.config.extras.js') 66 | .catch(function(reason) { 67 | console.log( 68 | 'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.' 69 | ); 70 | console.log(reason); 71 | }); 72 | } 73 | 74 | function initTestBed(){ 75 | return Promise.all([ 76 | System.import('@angular/core/testing'), 77 | System.import('@angular/platform-browser-dynamic/testing') 78 | ]) 79 | 80 | .then(function (providers) { 81 | var coreTesting = providers[0]; 82 | var browserTesting = providers[1]; 83 | 84 | coreTesting.TestBed.initTestEnvironment( 85 | browserTesting.BrowserDynamicTestingModule, 86 | browserTesting.platformBrowserDynamicTesting()); 87 | }) 88 | } 89 | 90 | // Import all spec files and start karma 91 | function initTesting () { 92 | return Promise.all( 93 | allSpecFiles.map(function (moduleName) { 94 | return System.import(moduleName); 95 | }) 96 | ) 97 | .then(__karma__.start, __karma__.error); 98 | } 99 | -------------------------------------------------------------------------------- /file-utility.js: -------------------------------------------------------------------------------- 1 | /* 2 | After specifying the output folder for the Typescript compiler it was noted that the compiler does not copy 3 | the accompany html & css files into the output folder(s). This utility program creates a folder structure similar to 4 | the one Typescript would have created and copies the html and css files into the relevant folder. 5 | */ 6 | 7 | const fs = require('fs'); 8 | const chokidar = require('chokidar'); 9 | 10 | const watcher = chokidar.watch('src/app'); 11 | 12 | /** 13 | * Gets paths of files in a directory 14 | * 15 | * @param directory path to the current directory 16 | * @returns {Array} list of paths to files in the directory 17 | */ 18 | function getFilePaths(directory) { 19 | const filePaths = []; 20 | 21 | fs.readdirSync(directory) 22 | .forEach((fileInDirectory) => { 23 | const extension = fileInDirectory.split('.').pop(); 24 | if (extension === 'html' || extension === 'css') { 25 | const pathToFile = directory + fileInDirectory; 26 | filePaths.push(pathToFile); 27 | }}); 28 | return filePaths; 29 | } 30 | 31 | /** 32 | * Gets paths of child directories 33 | * @param rootDir path to directory 34 | * @returns {string[]} paths of child directories 35 | */ 36 | function getDirPaths(rootDir) { 37 | return fs.readdirSync(rootDir) 38 | .filter((childDir) => { 39 | const path = rootDir + childDir; 40 | 41 | const isDir = fs.lstatSync(path).isDirectory(); 42 | if (isDir) return path; 43 | }) 44 | } 45 | 46 | let paths = []; 47 | 48 | /** 49 | * Gets paths to all hmtl & css files in the root directory and child directories recursively. 50 | * 51 | * @param rootDir path to the root directory. 52 | */ 53 | function getPaths(rootDir) { 54 | const files = getFilePaths(rootDir); 55 | const dirs = getDirPaths(rootDir); 56 | 57 | const hasFiles = files.length > 1; 58 | const hasDirs = dirs.length > 1; 59 | 60 | if (hasFiles) { 61 | paths = paths.concat(files); 62 | } 63 | 64 | if (hasDirs) { 65 | dirs.forEach((directory) => { 66 | const newPath = rootDir + directory + '/'; 67 | getPaths(newPath) 68 | }) 69 | } 70 | } 71 | 72 | /* 73 | Typescript creates a similar folder structure as this function. The reason why we have to manually create these folders 74 | is that this script is run before typescript has compiled. We need these folders to copy html and css files into. 75 | */ 76 | function createDirs() { 77 | const src = 'src/app/'; 78 | const compilerOutput = 'src/compiler-output'; 79 | 80 | getPaths(src); 81 | 82 | if (!fs.existsSync(compilerOutput)){ 83 | fs.mkdirSync(compilerOutput); 84 | } 85 | 86 | paths.forEach((path) => { 87 | const dest = path.replace('src/app/', 'src/compiler-output/app/'); 88 | let dir = dest.split('/'); 89 | dir.pop(); 90 | dir = dir.join('/'); 91 | 92 | if (!fs.existsSync(dir)){ 93 | fs.mkdirSync(dir); 94 | } 95 | }) 96 | } 97 | 98 | /* 99 | The challenge with specifying the output folder for the typescript compiler is that Typescript does not output 100 | the companion html and css files into these folders as well. So we have to copy these files into the directories 101 | Typescript outputs to. 102 | */ 103 | function copyFiles() { 104 | paths.forEach((path) => { 105 | const dest = path.replace('src/app/', 'src/compiler-output/app/'); 106 | 107 | fs.copyFileSync(path, dest); 108 | }) 109 | } 110 | 111 | createDirs(); 112 | copyFiles(); 113 | 114 | // Watches for changes and copies the css & html files 115 | function watch() { 116 | watcher 117 | .on('add', () => copyFiles()) 118 | .on('change', () => copyFiles()); 119 | } 120 | 121 | watch(); 122 | 123 | -------------------------------------------------------------------------------- /src/app/GraphExplorerComponent.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { getRequestBodyEditor } from './api-explorer-jseditor'; 7 | import { AppComponent } from './app.component'; 8 | import { isAuthenticated as isAuthHelper } from './authentication/auth'; 9 | import { IGraphApiCall, IGraphRequestHeader, ISampleQuery, substituteTokens } from './base'; 10 | import { getString } from './localization-helpers'; 11 | import { QueryRunnerService } from './query-runner.service'; 12 | import { getGraphUrl } from './util'; 13 | 14 | export class GraphExplorerComponent { 15 | 16 | public explorerValues = AppComponent.explorerValues; 17 | 18 | public getStr(label: string): string { 19 | return getString(AppComponent.Options, label) || '*****' + label; 20 | } 21 | 22 | public getAssetPath(relPath: string): string { 23 | return AppComponent.Options.PathToBuildDir + '/' + relPath; 24 | } 25 | 26 | // Used in sidebar and panel 27 | public getRequestHistory = (limit?: number): IGraphApiCall[] => { 28 | if (limit) { 29 | return AppComponent.requestHistory.slice(0, limit); 30 | } 31 | 32 | return AppComponent.requestHistory; 33 | } 34 | 35 | public isAuthenticated() { 36 | return isAuthHelper(); 37 | } 38 | 39 | public loadQueryIntoEditor(originalQuery: IGraphApiCall) { 40 | // Prevent logged out users from POSTing/others 41 | if (!this.isAuthenticated() && originalQuery.method !== 'GET') { 42 | return; 43 | } 44 | 45 | QueryRunnerService.clearResponse(); 46 | 47 | // Copy the sample query or history item so we're not changing history/samples 48 | const query: ISampleQuery = jQuery.extend(true, {}, originalQuery); 49 | substituteTokens(query); 50 | 51 | // Set the endpoint url. if it's a relative path, add the configured graph URL 52 | AppComponent.explorerValues.endpointUrl = query.requestUrl.startsWith('https://') ? query.requestUrl : 53 | getGraphUrl() + query.requestUrl; 54 | AppComponent.explorerValues.selectedOption = query.method; 55 | 56 | if (query.headers) { // tslint:disable-line 57 | AppComponent.explorerValues.headers = query.headers; 58 | } else { 59 | AppComponent.explorerValues.headers = []; 60 | } 61 | 62 | this.shouldEndWithOneEmptyHeader(); 63 | 64 | AppComponent.explorerValues.postBody = ''; 65 | const postBodyEditorSession = getRequestBodyEditor().getSession(); 66 | if (query.postBody) { 67 | const rawPostBody = query.postBody; 68 | 69 | AppComponent.explorerValues.postBody = rawPostBody; 70 | // Try to format the post body 71 | 72 | let formattedPostBody; 73 | try { 74 | formattedPostBody = JSON.stringify(JSON.parse(rawPostBody), null, 2); 75 | } catch (e) { 76 | throw (e); 77 | } 78 | 79 | AppComponent.explorerValues.postBody = formattedPostBody || rawPostBody; 80 | } 81 | 82 | postBodyEditorSession.setValue(AppComponent.explorerValues.postBody); 83 | } 84 | public shouldEndWithOneEmptyHeader() { 85 | const lastHeader = this.getLastHeader(); 86 | if (lastHeader && lastHeader.name === '' && lastHeader.value === '') { 87 | return; 88 | } else { 89 | this.addEmptyHeader(); 90 | } 91 | } 92 | 93 | public addEmptyHeader() { 94 | AppComponent.explorerValues.headers.push({ 95 | name: '', 96 | value: '', 97 | }); 98 | } 99 | 100 | public getLastHeader(): IGraphRequestHeader { 101 | return this.explorerValues.headers[this.explorerValues.headers.length - 1]; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /util-scripts/query-importer.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | var csv = require("fast-csv"); 3 | 4 | let stream = fs.createReadStream('sample-queries.csv') 5 | 6 | let knownHeaders = { 7 | "Category Name": "category", 8 | "Type of Query": "method", 9 | "Query title (max length: 64 chars)": "humanName", 10 | "Query URL": "requestUrl", 11 | "Doc Link": "docLink", 12 | "Post template name": "postBodyTemplateName", 13 | "Headers": 'headers', 14 | "Post template": 'postBody', 15 | "Tip (something we'll expose in the UI when a user makes a particular request)": "tip" 16 | } 17 | 18 | let schema, queries = []; 19 | 20 | var csvStream = csv() 21 | .on("data", (line) => { 22 | 23 | if (!schema) { 24 | schema = {}; 25 | let headers = line; 26 | for (let i = 0; i < headers.length; i++) { 27 | schema[headers[i]] = i; 28 | } 29 | return; 30 | } 31 | 32 | let query = createQueryFromLine(line) 33 | 34 | if (query["Query URL"]) // only add queries that have URLs 35 | queries.push(query); 36 | }) 37 | .on("end", function () { 38 | saveSampleQueries(); 39 | }); 40 | 41 | function createQueryFromLine(lineArr) { 42 | 43 | var query = {}; 44 | for (let col in schema) { 45 | if (lineArr[schema[col]]) // don't add empty cells 46 | query[col] = lineArr[schema[col]]; 47 | } 48 | return query; 49 | } 50 | 51 | stream.pipe(csvStream); 52 | 53 | function convertRawQueryToSampleQueryType(query) { 54 | let sampleQuery = {} 55 | for (let knownHeadersCSVColName in knownHeaders) { 56 | if (knownHeadersCSVColName in query) { 57 | sampleQuery[knownHeaders[knownHeadersCSVColName]] = query[knownHeadersCSVColName]; 58 | } 59 | } 60 | return sampleQuery; 61 | } 62 | 63 | function saveSampleQueries() { 64 | let outStr = ` 65 | // ------------------------------------------------------------------------------ 66 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. 67 | // ------------------------------------------------------------------------------ 68 | 69 | // WARNING - This file is generated from util-scripts/query-importer.js 70 | 71 | import { ISampleQuery } from "../base"; 72 | 73 | export const SampleQueries: ISampleQuery[] = [ 74 | `; 75 | formattedQueryArr = [] 76 | for (let i = 0; i < queries.length; i++) { 77 | let csvQuery = queries[i]; 78 | let sampleQuery = convertRawQueryToSampleQueryType(csvQuery); 79 | sampleQuery.skipTest = false; 80 | 81 | cleanupSampleQuery(sampleQuery); 82 | 83 | formattedQueryArr.push(JSON.stringify(sampleQuery, null, 4)); 84 | } 85 | 86 | outStr += formattedQueryArr.join(",\n"); 87 | 88 | outStr += ']' 89 | 90 | fs.writeFile("src/app/generate-queries/gen-queries.ts", outStr, function (err) { 91 | if (err) { 92 | return console.log(err); 93 | } 94 | 95 | console.log("The file was saved!"); 96 | }); 97 | } 98 | 99 | function cleanupSampleQuery(sampleQuery) { 100 | if (sampleQuery.method) 101 | sampleQuery.method = sampleQuery.method.toUpperCase().trim(); 102 | 103 | // remove quotes on URL 104 | 105 | if (sampleQuery.requestUrl && sampleQuery.requestUrl[0] == '"') { 106 | try { 107 | sampleQuery.requestUrl = JSON.parse(sampleQuery.requestUrl); 108 | } catch (e) { 109 | console.log(e); 110 | } 111 | } 112 | 113 | if (sampleQuery.docLink && sampleQuery.docLink[0] == '"') 114 | sampleQuery.docLink = JSON.parse(sampleQuery.docLink) 115 | 116 | 117 | if (sampleQuery.headers) { 118 | let headers = sampleQuery.headers.split(/[\r\n]+/); 119 | sampleQuery.headers = []; 120 | for (let header of headers) { 121 | if (!header) continue; 122 | let name = header.split(":")[0].trim(); 123 | let value = header.split(/:(.+)/)[1].trim(); 124 | sampleQuery.headers.push({ 125 | name: name, 126 | value: value 127 | }) 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/app/main-column/main-column.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
10 | 18 |
19 | 20 | 21 |
22 |
23 | 26 |
27 |
28 | 29 | 41 | 42 | 47 |
48 | 49 |
50 | 51 | 52 | 53 |
54 | 62 | 63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /src/app/graph-service.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { Injectable } from '@angular/core'; 7 | import { Headers, Http, Response, ResponseContentType } from '@angular/http'; 8 | import 'rxjs/add/operator/toPromise'; 9 | import { acquireNewAccessToken } from './authentication/auth.service'; 10 | import { app } from './authentication/msal-user-agent'; 11 | import { AllowedGraphDomains, RequestType } from './base'; 12 | 13 | @Injectable() 14 | export class GraphService { 15 | public app: any; 16 | 17 | constructor(private http: Http) { 18 | this.app = app; 19 | } 20 | 21 | public performAnonymousQuery(queryType: RequestType, query: string, headers?: Headers): Promise { 22 | if (!headers) { 23 | headers = new Headers(); 24 | } 25 | headers.append('Authorization', 'Bearer {token:https://graph.microsoft.com/}'); 26 | headers.append('SdkVersion', 'GraphExplorer/3.0'); 27 | 28 | if (queryType === 'GET') { 29 | return this.http 30 | .get(`https://proxy.apisandbox.msdn.microsoft.com/svc?url=${encodeURIComponent(query)}`, { headers }) 31 | .toPromise(); 32 | } else if (queryType === 'GET_BINARY') { 33 | return this.http 34 | .get(`https://proxy.apisandbox.msdn.microsoft.com/svc?url=${encodeURIComponent(query)}`, 35 | { headers, responseType: ResponseContentType.ArrayBuffer }).toPromise(); 36 | } 37 | } 38 | 39 | public performQuery = (queryType: RequestType, query: string, postBody?: any, requestHeaders?: Headers) => { 40 | // Make sure the request is being sent to the Graph and not another domain 41 | let sentToGraph = false; 42 | 43 | for (const domain of AllowedGraphDomains) { 44 | if (query.startsWith(domain)) { 45 | sentToGraph = true; 46 | break; 47 | } 48 | } 49 | 50 | if (!sentToGraph) { 51 | throw new Error('Not sending request to known Graph deployment'); 52 | } 53 | 54 | if (typeof requestHeaders === 'undefined') { 55 | requestHeaders = new Headers(); 56 | } 57 | requestHeaders.append('SdkVersion', 'GraphExplorer/3.0'); 58 | 59 | const queryResult = this.handleRequest(this.app, requestHeaders, query, queryType, postBody); 60 | return queryResult; 61 | } 62 | 63 | public getMetadata = (graphUrl: string, version: string) => { 64 | return this.http.get(`${graphUrl}/${version}/$metadata`).toPromise(); 65 | } 66 | 67 | public handleRequest = async (msalUserAgent, requestHeaders, query, queryType, postBody): Promise => { 68 | return acquireNewAccessToken(msalUserAgent).then((response) => { 69 | requestHeaders.append('Authorization', `Bearer ${response.accessToken}`); 70 | switch (queryType) { 71 | case 'GET': 72 | return this.http.get(query, { headers: requestHeaders }).toPromise(); 73 | case 'GET_BINARY': 74 | return this.http.get(query, 75 | { responseType: ResponseContentType.ArrayBuffer, headers: requestHeaders }) 76 | .toPromise(); 77 | case 'PUT': 78 | return this.http.put(query, postBody, { headers: requestHeaders }).toPromise(); 79 | case 'POST': 80 | return this.http.post(query, postBody, { headers: requestHeaders }).toPromise(); 81 | case 'PATCH': 82 | return this.http.patch(query, postBody, { headers: requestHeaders }).toPromise(); 83 | case 'DELETE': 84 | return this.http.delete(query, { headers: requestHeaders }).toPromise(); 85 | } 86 | }); 87 | }; 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/app/scopes-dialog/scopes-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 14 |
{{getStr('modify permissions')}}
15 |

Select different 16 | permissions 17 | to try out Microsoft Graph API endpoints.

18 |
19 |
21 | 22 | 23 | 36 | 41 | 42 |
24 |
25 | 34 |
35 |
37 | 38 | Admin 39 | 40 |
43 |
44 |
45 |
46 |
47 |
48 | 49 |
50 | 54 |
55 |
56 |
57 |
58 |
59 | 63 | 66 | 74 |
75 |
76 |
77 | -------------------------------------------------------------------------------- /src/app/tokens.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from 'guid-typescript'; 2 | 3 | import { AppComponent } from './app.component'; 4 | import { IToken } from './base'; 5 | /** 6 | * For more information on Tokens, see the IToken interface definition 7 | * in base.ts. This is an unordered list of all tokens that can supply 8 | * values for POST body templates and URL endpoints for both the demo 9 | * tenant and authenticated users. The demoTenantValue and 10 | * authenticatedUserValue fields are checked first, and then the 11 | * defaultValue fields. 12 | */ 13 | 14 | export const Tokens: IToken[] = [ 15 | { 16 | placeholder: 'group-id', 17 | demoTenantValue: '02bd9fd6-8f93-4758-87c3-1fb73740a315', 18 | }, 19 | { 20 | placeholder: 'drive-item-id', 21 | demoTenantValue: '01BYE5RZZ5OJSCSRM6BZDY7ZEFZ3NJ2QAY', 22 | }, 23 | { 24 | placeholder: 'section-id', 25 | demoTenantValue: '1-fb22b2f1-379f-4da4-bf7b-be5dcca7b99a', 26 | }, 27 | { 28 | placeholder: 'notebook-id', 29 | demoTenantValue: '1-fb22b2f1-379f-4da4-bf7b-be5dcca7b99a', 30 | }, 31 | { 32 | placeholder: 'group-id-with-plan', 33 | demoTenantValue: '1e770bc2-3c5f-487f-871f-16fbdf1c8ed8', 34 | }, 35 | { 36 | placeholder: 'plan-id', 37 | demoTenantValue: 'CONGZUWfGUu4msTgNP66e2UAAySi', 38 | }, 39 | { 40 | placeholder: '{bucket-id}', 41 | demoTenantValue: '1m6FwcAAZ0eW5J1Abe7ndWUAJ1ca', 42 | }, 43 | { 44 | placeholder: '{bucket-name}', 45 | demoTenantValue: 'New Bucket', 46 | }, 47 | { 48 | placeholder: 'task-id', 49 | demoTenantValue: 'oIx3zN98jEmVOM-4mUJzSGUANeje', 50 | }, 51 | { 52 | placeholder: 'task-title', 53 | defaultValue: 'New Task', 54 | }, 55 | { 56 | placeholder: 'extension-id', 57 | demoTenantValue: 'com.contoso.roamingSettings', 58 | }, 59 | { 60 | placeholder: 'host-name', 61 | demoTenantValue: 'M365x214355.sharepoint.com', 62 | }, 63 | { 64 | placeholder: 'server-relative-path', 65 | demoTenantValue: 'sites/contoso/Departments/SM/MarketingDocuments', 66 | }, 67 | { 68 | placeholder: 'group-id-for-teams', 69 | demoTenantValue: '02bd9fd6-8f93-4758-87c3-1fb73740a315', 70 | }, 71 | { 72 | placeholder: 'team-id', 73 | demoTenantValue: '02bd9fd6-8f93-4758-87c3-1fb73740a315', 74 | }, 75 | { 76 | placeholder: 'channel-id', 77 | demoTenantValue: '19:d0bba23c2fc8413991125a43a54cc30e@thread.skype', 78 | }, 79 | { 80 | placeholder: 'message-id', 81 | demoTenantValue: '1501527481624', 82 | }, 83 | { 84 | placeholder: 'reply-id', 85 | demoTenantValue: '1501527483334', 86 | }, 87 | { 88 | placeholder: 'application-id', 89 | demoTenantValue: 'acc848e9-e8ec-4feb-a521-8d58b5482e09', 90 | }, 91 | { 92 | placeholder: 'destination-address', 93 | demoTenantValue: '1.2.3.5', 94 | }, 95 | { 96 | placeholder: 'today', 97 | defaultValueFn: () => { 98 | return (new Date()).toISOString(); 99 | }, 100 | }, 101 | { 102 | placeholder: 'todayMinusHour', 103 | defaultValueFn: () => { 104 | const todayMinusHour = new Date(); 105 | todayMinusHour.setHours(new Date().getHours() - 1); 106 | return todayMinusHour.toISOString(); 107 | }, 108 | }, 109 | { 110 | placeholder: 'coworker-mail', 111 | demoTenantValue: 'meganb@M365x214355.onmicrosoft.com', 112 | authenticatedUserValueFn: () => { 113 | return AppComponent.explorerValues.authentication.user.emailAddress; 114 | }, 115 | }, 116 | { 117 | placeholder: 'next-week', 118 | defaultValueFn: () => { 119 | const today = new Date(); 120 | const nextWeek = new Date(); 121 | nextWeek.setDate(today.getDate() + 7); 122 | return nextWeek.toISOString(); 123 | }, 124 | }, 125 | { 126 | placeholder: 'user-mail', 127 | demoTenantValue: 'MiriamG@M365x214355.onmicrosoft.com', 128 | authenticatedUserValueFn: () => { 129 | return AppComponent.explorerValues.authentication.user.emailAddress; 130 | }, 131 | }, 132 | { 133 | placeholder: 'domain', 134 | defaultValueFn: () => { 135 | return 'contoso.com'; 136 | }, 137 | authenticatedUserValueFn: () => { 138 | return AppComponent.explorerValues.authentication.user.emailAddress.split('@')[1]; 139 | }, 140 | }, 141 | { 142 | placeholder: 'list-id', 143 | defaultValue: 'd7689e2b-941a-4cd3-bb24-55cddee54294', 144 | }, 145 | { 146 | placeholder: 'list-title', 147 | defaultValue: 'Contoso Home', 148 | }, 149 | { 150 | placeholder: 'Placeholder Password', 151 | defaultValue: Guid.create().toString(), 152 | }, 153 | ]; 154 | -------------------------------------------------------------------------------- /src/app/generate-queries/gen-queries.spec.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | // tslint:disable-next-line 7 | // gen-queries.spec.ts prepares non destructive sample queries and runs the tests. 8 | 9 | import { Headers, HttpModule } from '@angular/http'; 10 | 11 | import { 12 | inject, 13 | TestBed, 14 | } from '@angular/core/testing'; // tslint:disable-line 15 | 16 | import { localLogout } from '../authentication/auth'; 17 | import { GraphApiVersion, GraphApiVersions, IGraphRequestHeader, substituteTokens } from '../base'; 18 | import { GraphService } from '../graph-service'; 19 | import { SampleQueries } from './gen-queries'; 20 | 21 | function getGraphVersionFromUrl(url: string): GraphApiVersion { 22 | for (const version of GraphApiVersions) { 23 | if (url.indexOf(`/${version}/`) !== -1) { 24 | return version; 25 | } 26 | } 27 | } 28 | 29 | // Convert from GraphRequestHeaders to Fetch API headers. 30 | function convertHeaders(graphRequestHeaders: IGraphRequestHeader[]): Headers { 31 | const headers = new Headers(); 32 | 33 | if (graphRequestHeaders) { 34 | for (let i = 0; i < graphRequestHeaders.length; ++i) { // tslint:disable-line 35 | headers.append(graphRequestHeaders[i].name, graphRequestHeaders[i].value); 36 | } 37 | } 38 | 39 | return headers; 40 | } 41 | 42 | let graphService: GraphService; 43 | describe('Sample query validation', () => { 44 | beforeEach(() => { 45 | localLogout(); 46 | 47 | TestBed.configureTestingModule({ 48 | imports: [HttpModule], 49 | providers: [GraphService], 50 | }); 51 | graphService = TestBed.get(GraphService); 52 | }); 53 | 54 | // tslint:disable-next-line 55 | it('Creates an instance of the graph service', inject([GraphService], (_graphService: GraphService) => { 56 | graphService = _graphService; 57 | })); 58 | 59 | for (const query of SampleQueries) { 60 | it(`${query.humanName}: Doc link should exist and match request version`, () => { 61 | if (!query.docLink) { 62 | throw new Error(`${query.humanName}: Doc link doesn't exist`); 63 | } 64 | 65 | const docLinkVersion = getGraphVersionFromUrl(query.docLink); 66 | const requestUrlVersion = getGraphVersionFromUrl(query.requestUrl); 67 | 68 | // Some doc links go to concept pages, not /version/doc page 69 | if (docLinkVersion && requestUrlVersion) { 70 | expect(docLinkVersion).toBe(requestUrlVersion); 71 | } 72 | }); 73 | 74 | if (query.method !== 'GET') { 75 | continue; 76 | } 77 | if (query.skipTest) { 78 | continue; 79 | } 80 | substituteTokens(query); 81 | it(`GET query should execute: ${query.humanName}`, (done) => { 82 | substituteTokens(query); 83 | 84 | /** 85 | * Indicates whether we will skip the named query response length check. 86 | * @returns {Boolean} - A value of true indicates that the we should skip the response length check. 87 | */ 88 | function skipResponseLengthCheck(): boolean { 89 | 90 | /* 91 | A list of query names from gen-queries.ts that we will skip. 92 | These are the names of samples where we expect to a get a response body 93 | that contains an empty JSON object. 94 | We are using this to skip the empty response check. 95 | */ 96 | const skipQueryList = [ 97 | 'get recent user activities', 98 | ]; 99 | 100 | skipQueryList.map((queryName) => { 101 | if (queryName === query.humanName) { 102 | return true; 103 | } 104 | }); 105 | 106 | return false; 107 | } 108 | 109 | const headers = convertHeaders(query.headers); 110 | 111 | graphService.performAnonymousQuery(query.method, 'https://graph.microsoft.com' + query.requestUrl, headers) 112 | .then((res) => { 113 | if (res.headers.get('Content-Type').indexOf('application/json') !== -1) { 114 | const response = res.json(); 115 | if (response && response.value && response.value.constructor === Array) { 116 | if (response.value.length === 0 && skipResponseLengthCheck()) { 117 | done.fail(`${query.humanName}: All sample GETs on collections must have values`); 118 | } 119 | } 120 | } 121 | done(); 122 | }).catch((e: Response) => { 123 | if (e.status < 500) { 124 | done.fail(`${query.humanName}: Can't execute sample GET request, ${e.status}, ${JSON.stringify(e.json())}`); 125 | } 126 | done(); 127 | }); 128 | }); 129 | } 130 | }); 131 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | 3 | var appBase = 'src/'; // transpiled app JS and map files 4 | var appSrcBase = appBase; // app source TS files 5 | 6 | // Testing helpers (optional) are conventionally in a folder called `testing` 7 | var testingBase = 'testing/'; // transpiled test JS and map files 8 | var testingSrcBase = 'testing/'; // test source TS files 9 | 10 | // http://karma-runner.github.io/2.0/config/configuration-file.html 11 | var configuration = { 12 | basePath: '', 13 | frameworks: ['jasmine'], 14 | 15 | plugins: [ 16 | require('karma-jasmine'), 17 | require('karma-chrome-launcher'), 18 | require('karma-jasmine-html-reporter') 19 | ], 20 | 21 | client: { 22 | builtPaths: [appBase, testingBase], // add more spec base paths as needed 23 | clearContext: false // leave Jasmine Spec Runner output visible in browser 24 | }, 25 | 26 | customLaunchers: { 27 | // From the CLI. Not used here but interesting 28 | // chrome setup for travis CI using chromium 29 | Chrome_travis_ci: { 30 | base: 'Chrome', 31 | flags: ['--no-sandbox'] 32 | }, 33 | ChromeHeadless: { 34 | base: 'Chrome', 35 | flags: [ 36 | '--no-sandbox', 37 | // See https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md 38 | '--headless', 39 | '--disable-gpu', 40 | // Without a remote debugging port, Google Chrome exits immediately. 41 | ' --remote-debugging-port=9222', 42 | ] 43 | } 44 | }, 45 | 46 | files: [ 47 | // System.js for module loading 48 | 'node_modules/systemjs/dist/system.src.js', 49 | 50 | // Polyfills 51 | 'node_modules/core-js/client/shim.js', 52 | 53 | // zone.js 54 | 'node_modules/zone.js/dist/zone.js', 55 | 'node_modules/zone.js/dist/long-stack-trace-zone.js', 56 | 'node_modules/zone.js/dist/proxy.js', 57 | 'node_modules/zone.js/dist/sync-test.js', 58 | 'node_modules/zone.js/dist/jasmine-patch.js', 59 | 'node_modules/zone.js/dist/async-test.js', 60 | 'node_modules/zone.js/dist/fake-async-test.js', 61 | 'node_modules/moment/min/moment-with-locales.min.js', 62 | 'node_modules/jquery/dist/jquery.min.js', 63 | 64 | // guid-typescript 65 | { pattern: 'node_modules/guid-typescript/dist/guid.js', included: false, watched: false }, 66 | 67 | { pattern: 'node_modules/@microsoft/applicationinsights-web/dist/applicationinsights-web.min.js', included: false, watched: false }, 68 | 69 | // RxJs 70 | { pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false }, 71 | { pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false }, 72 | 73 | { pattern: 'node_modules/msal/**/*.js', included: false, watched: false }, 74 | 75 | // Paths loaded via module imports: 76 | // Angular itself 77 | { pattern: 'node_modules/@angular/**/*.js', included: false, watched: false }, 78 | { pattern: 'node_modules/@angular/**/*.js.map', included: false, watched: false }, 79 | 80 | { pattern: 'systemjs.config.js', included: false, watched: false }, 81 | { pattern: appBase + '/systemjs.config.extras.js', included: false, watched: false }, 82 | 'karma-test-shim.js', // optionally extend SystemJS mapping e.g., with barrels 83 | 84 | // transpiled application & spec code paths loaded via module imports 85 | { pattern: appBase + '**/*.js', included: false, watched: true }, 86 | { pattern: testingBase + '**/*.js', included: false, watched: true }, 87 | 88 | 89 | // Asset (HTML & CSS) paths loaded via Angular's component compiler 90 | // (these paths need to be rewritten, see proxies section) 91 | { pattern: appBase + '**/*.html', included: false, watched: true }, 92 | { pattern: appBase + '**/*.css', included: false, watched: true }, 93 | 94 | // Paths for debugging with source maps in dev tools 95 | { pattern: appBase + '**/*.ts', included: false, watched: false }, 96 | { pattern: appBase + '**/*.js.map', included: false, watched: false }, 97 | { pattern: testingSrcBase + '**/*.ts', included: false, watched: false }, 98 | { pattern: testingBase + '**/*.js.map', included: false, watched: false }, 99 | 100 | ], 101 | 102 | // Proxied base paths for loading assets 103 | proxies: { 104 | // required for modules fetched by SystemJS 105 | '/base/src/node_modules/': '/base/node_modules/' 106 | }, 107 | 108 | exclude: [], 109 | preprocessors: {}, 110 | reporters: ['progress', 'kjhtml'], 111 | 112 | port: 9876, 113 | colors: true, 114 | logLevel: config.LOG_ERROR, 115 | autoWatch: true, 116 | browsers: ['Chrome'], 117 | singleRun: false, 118 | 119 | // Adding this since we are getting a timeout in CI. The default at 10 seconds was not enough. 120 | browserNoActivityTimeout: 1000 * 30 121 | } 122 | 123 | config.set(configuration); 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/app/response-handlers.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { getAceEditorFromElId, getJsonViewer } from './api-explorer-jseditor'; 7 | 8 | export function showResults(results, responseContentType) { 9 | getJsonViewer().setValue(''); 10 | getJsonViewer().getSession().insert(0, results); 11 | if (responseContentType) { 12 | getJsonViewer().getSession().setMode('ace/mode/' + responseContentType); 13 | } 14 | } 15 | 16 | export function insertHeadersIntoResponseViewer(headers: Headers) { 17 | if (!headers) { 18 | return; // Prevents foreach of undefined error 19 | } 20 | 21 | // Format headers 22 | const headersArr = []; 23 | headers.forEach((headerValue, headerKey) => { 24 | headersArr.push(headerKey + ': ' + headerValue); 25 | }); 26 | 27 | getAceEditorFromElId('response-header-viewer').getSession().setValue(''); 28 | getAceEditorFromElId('response-header-viewer').getSession().insert(0, headersArr.join('\n')); 29 | } 30 | 31 | export function handleHtmlResponse(results) { 32 | showResults(results, 'html'); 33 | } 34 | 35 | export function handleJsonResponse(results) { 36 | results = JSON.stringify(results, null, 4); 37 | showResults(results, 'json'); 38 | } 39 | 40 | export function handleXmlResponse(results) { 41 | results = formatXml(results); 42 | showResults(results, 'xml'); 43 | } 44 | 45 | export function handleTextResponse(results) { 46 | showResults(results, 'plain_text'); 47 | } 48 | 49 | export function isImageResponse(contentType: string) { 50 | return contentType === 'application/octet-stream' || contentType.substr(0, 6) === 'image/'; 51 | } 52 | 53 | export function getContentType(headers: Headers) { 54 | const full = headers.get('content-type'); 55 | const delimiterPos = full.indexOf(';'); 56 | if (delimiterPos !== -1) { 57 | return full.substr(0, delimiterPos); 58 | } else { 59 | return full; 60 | } 61 | } 62 | 63 | // From swagger-js 64 | const formatXml = (xml) => { 65 | let contexp; 66 | let fn; 67 | let formatted; 68 | let indent; 69 | let l; 70 | let lastType; 71 | let len; 72 | let lines; 73 | let ln; 74 | let pad; // tslint:disable-line 75 | let reg; 76 | let transitions; 77 | let wsexp; 78 | reg = /(>)(<)(\/*)/g; 79 | wsexp = /[ ]*(.*)[ ]+\n/g; 80 | contexp = /(<.+>)(.+\n)/g; 81 | xml = xml.replace(reg, '$1\n$2$3').replace(wsexp, '$1\n').replace(contexp, '$1\n$2'); 82 | pad = 0; 83 | formatted = ''; 84 | lines = xml.split('\n'); 85 | indent = 0; 86 | lastType = 'other'; 87 | transitions = { 88 | 'single->single': 0, 89 | 'single->closing': -1, 90 | 'single->opening': 0, 91 | 'single->other': 0, 92 | 'closing->single': 0, 93 | 'closing->closing': -1, 94 | 'closing->opening': 0, 95 | 'closing->other': 0, 96 | 'opening->single': 1, 97 | 'opening->closing': 0, 98 | 'opening->opening': 1, 99 | 'opening->other': 1, 100 | 'other->single': 0, 101 | 'other->closing': -1, 102 | 'other->opening': 0, 103 | 'other->other': 0, 104 | }; 105 | fn = (ln) => { //tslint:disable-line 106 | let fromTo; 107 | let j; 108 | let key; 109 | let padding; 110 | let type; 111 | let types; 112 | let value; 113 | types = { 114 | single: Boolean(ln.match(/<.+\/>/)), 115 | closing: Boolean(ln.match(/<\/.+>/)), 116 | opening: Boolean(ln.match(/<[^!?].*>/)), 117 | }; 118 | type = ((() => { 119 | let results; 120 | results = []; 121 | for (key in types) { // tslint:disable-line 122 | value = types[key]; 123 | if (value) { 124 | results.push(key); 125 | } 126 | } 127 | return results; 128 | })())[0]; 129 | type = type === void 0 ? 'other' : type; 130 | fromTo = lastType + '->' + type; 131 | lastType = type; 132 | padding = ''; 133 | indent += transitions[fromTo]; 134 | padding = ((() => { 135 | let m; 136 | let ref1; 137 | let results; 138 | results = []; 139 | // tslint:disable-next-line 140 | for (j = m = 0, ref1 = indent; 0 <= ref1 ? m < ref1 : m > ref1; j = 0 <= ref1 ? ++m : --m) { 141 | results.push(' '); 142 | } 143 | return results; 144 | })()).join(''); 145 | if (fromTo === 'opening->closing') { 146 | formatted = formatted.substr(0, formatted.length - 1) + ln + '\n'; 147 | } else { 148 | formatted += padding + ln + '\n'; 149 | } 150 | }; 151 | for (l = 0, len = lines.length; l < len; l++) { //tslint:disable-line 152 | ln = lines[l]; 153 | fn(ln); 154 | } 155 | return formatted; 156 | }; 157 | -------------------------------------------------------------------------------- /src/app/graph-structure.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | import { HttpModule } from '@angular/http'; 3 | 4 | import { 5 | inject, 6 | TestBed, 7 | } from '@angular/core/testing'; 8 | 9 | import { GraphApiVersions } from './base'; 10 | import { GraphService } from './graph-service'; 11 | import { constructGraphLinksFromFullPath, parseMetadata } from './graph-structure'; 12 | 13 | let graphService: GraphService; 14 | 15 | /** 16 | * Expect statements are commented out because they were failing after an alias was added to the metadata. 17 | */ 18 | describe('Graph structural tests', () => { 19 | 20 | beforeEach(() => { 21 | TestBed.configureTestingModule({ 22 | imports: [HttpModule], 23 | providers: [GraphService], 24 | }); 25 | }); 26 | 27 | // tslint:disable-next-line 28 | it('Creates an instance of the graph service', inject([GraphService], (_graphService: GraphService) => { 29 | graphService = _graphService; 30 | })); 31 | 32 | for (const version of GraphApiVersions) { 33 | it(`should download ${version} metadata and build the graph structures(Entity,EntitySet,SingleTon) from it`, 34 | (done) => { 35 | return parseMetadata(graphService, version).then(done); 36 | }); 37 | } 38 | 39 | it('https://graph.microsoft.com/v1.0/me => [user]', () => { 40 | const links = constructGraphLinksFromFullPath('https://graph.microsoft.com/v1.0/me'); 41 | 42 | expect(links.length).toBe(1); 43 | expect(links[0].type).toBe('microsoft.graph.user'); 44 | }); 45 | 46 | it('https://graph.microsoft.com/v1.0/me/drive/quota => [user] -> [drive] -> [drive quoata]', () => { 47 | const links = constructGraphLinksFromFullPath('https://graph.microsoft.com/beta/me/drive/quota'); 48 | // expect(links.length).toBe(3); 49 | 50 | expect(links[0].type).toBe('microsoft.graph.user'); 51 | expect(links[0].isACollection).toBe(false); 52 | 53 | // expect(links[1].type).toBe('microsoft.graph.drive'); 54 | // expect(links[1].isACollection).toBe(false); 55 | 56 | // expect(links[2].type).toBe('microsoft.graph.quota'); 57 | // expect(links[2].isACollection).toBe(false); 58 | // expect(links[2].tagName).toBe('Property'); 59 | }); 60 | 61 | it('https://graph.microsoft.com/v1.0/users/foobar@contoso.com/calendar => [users] -> [user] -> [calendar]', () => { 62 | const links = constructGraphLinksFromFullPath( 63 | 'https://graph.microsoft.com/v1.0/users/foobar@contoso.com/calendar'); 64 | expect(links.length).toBe(3); 65 | 66 | expect(links[0].type).toBe('microsoft.graph.user'); 67 | expect(links[0].isACollection).toBe(true); 68 | 69 | expect(links[1].type).toBe('microsoft.graph.user'); 70 | expect(links[1].isACollection).toBe(false); 71 | expect(links[1].name).toBe('foobar@contoso.com'); 72 | 73 | // expect(links[2].type).toBe('microsoft.graph.calendar'); 74 | // expect(links[2].isACollection).toBe(false); 75 | 76 | }); 77 | 78 | it('https://graph.microsoft.com/beta/me/photos/ => [user] -> [profilePhoto collection]', () => { 79 | const links = constructGraphLinksFromFullPath('https://graph.microsoft.com/beta/me/photos/'); 80 | expect(links.length).toBe(2); 81 | 82 | expect(links[0].type).toBe('microsoft.graph.user'); 83 | expect(links[0].isACollection).toBe(false); 84 | 85 | // expect(links[1].type).toBe('microsoft.graph.profilePhoto'); 86 | // expect(links[1].isACollection).toBe(true); 87 | 88 | }); 89 | 90 | it('https://graph.microsoft.com/beta/me/photos/x/width => [user] -> [profilePhoto collection] -> ' + 91 | '[profilePhoto] -> [width property]', () => { 92 | const links = constructGraphLinksFromFullPath('https://graph.microsoft.com/beta/me/photos/x/width'); 93 | // expect(links.length).toBe(4); 94 | 95 | expect(links[0].type).toBe('microsoft.graph.user'); 96 | expect(links[0].isACollection).toBe(false); 97 | 98 | // expect(links[1].type).toBe('microsoft.graph.profilePhoto'); 99 | // expect(links[1].isACollection).toBe(true); 100 | 101 | // expect(links[2].type).toBe('microsoft.graph.profilePhoto'); 102 | // expect(links[2].isACollection).toBe(false); 103 | // expect(links[2].name).toBe('x'); 104 | 105 | // expect(links[3].name).toBe('width'); 106 | // expect(links[3].type).toBe('Edm.Int32'); 107 | // expect(links[3].tagName).toBe('Property'); 108 | }); 109 | 110 | it('https://graph.microsoft.com/beta/me/city => [microsoft.graph.user] -> [city property]', () => { 111 | const links = constructGraphLinksFromFullPath('https://graph.microsoft.com/beta/me/city'); 112 | expect(links.length).toBe(2); 113 | 114 | expect(links[0].type).toBe('microsoft.graph.user'); 115 | expect(links[0].isACollection).toBe(false); 116 | expect(links[0].tagName).toBe('Singleton'); 117 | 118 | expect(links[1].tagName).toBe('Property'); 119 | expect(links[1].isACollection).toBe(false); 120 | expect(links[1].name).toBe('city'); 121 | expect(links[1].type).toBe('Edm.String'); 122 | }); 123 | 124 | it('https://graph.microsoft.com/beta/me/drive/quota => [user] -> [drive] -> [drive quoata]', () => { 125 | const links = constructGraphLinksFromFullPath('https://graph.microsoft.com/beta/me/drive/quota'); 126 | // expect(links.length).toBe(3); 127 | 128 | expect(links[0].type).toBe('microsoft.graph.user'); 129 | expect(links[0].isACollection).toBe(false); 130 | 131 | // expect(links[1].type).toBe('microsoft.graph.drive'); 132 | // expect(links[1].isACollection).toBe(false); 133 | 134 | // expect(links[2].type).toBe('microsoft.graph.quota'); 135 | // expect(links[2].isACollection).toBe(false); 136 | // expect(links[2].tagName).toBe('Property'); 137 | 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { AfterViewInit, ChangeDetectorRef, Component, OnInit } from '@angular/core'; 7 | 8 | import { refreshAceEditorsContent } from './ace-utils'; 9 | import { localLogout } from './authentication/auth'; 10 | import { 11 | GraphApiVersion, GraphApiVersions, IExplorerOptions, IExplorerValues, IGraphApiCall, IMessage, 12 | IMessageBarContent, RequestType, 13 | } from './base'; 14 | import { initFabricComponents } from './fabric-components'; 15 | import { GenericDialogComponent } from './generic-message-dialog.component'; 16 | import { GraphService } from './graph-service'; 17 | import { parseMetadata } from './graph-structure'; 18 | import { GraphExplorerComponent } from './GraphExplorerComponent'; 19 | import { loadHistoryFromLocalStorage, saveHistoryToLocalStorage } from './history/history'; 20 | import { getGraphUrl, getParameterByName } from './util'; 21 | 22 | declare let mwfAutoInit; 23 | declare let moment; 24 | 25 | @Component({ 26 | selector: 'api-explorer', 27 | providers: [GraphService], 28 | templateUrl: './app.component.html', 29 | styles: [` 30 | #explorer-main { 31 | padding-left: 12px; 32 | } 33 | 34 | sidebar { 35 | padding: 0px; 36 | } 37 | `], 38 | }) 39 | export class AppComponent extends GraphExplorerComponent implements OnInit, AfterViewInit { 40 | 41 | public static messageBarContent: IMessageBarContent; 42 | public static _changeDetectionRef: ChangeDetectorRef; // tslint:disable-line 43 | public static message: IMessage; 44 | 45 | public static Options: IExplorerOptions = { 46 | ClientId: '', 47 | Language: 'en-US', 48 | // tslint:disable-next-line:max-line-length 49 | DefaultUserScopes: ['openid', 'profile', 'User.Read'], 50 | AuthUrl: 'https://login.microsoftonline.com', 51 | GraphVersions: GraphApiVersions, 52 | PathToBuildDir: '', 53 | }; 54 | 55 | public static explorerValues: IExplorerValues = { 56 | selectedOption: getParameterByName('method') as RequestType || 'GET', 57 | selectedVersion: getParameterByName('version') as GraphApiVersion || 'v1.0', 58 | authentication: { 59 | user: {}, 60 | }, 61 | showImage: false, 62 | requestInProgress: false, 63 | headers: [], 64 | postBody: '', 65 | }; 66 | 67 | public static requestHistory: IGraphApiCall[] = loadHistoryFromLocalStorage(); 68 | 69 | public static addRequestToHistory(request: IGraphApiCall) { 70 | AppComponent.requestHistory.splice(0, 0, request); // Add history object to the array 71 | saveHistoryToLocalStorage(AppComponent.requestHistory); 72 | } 73 | 74 | public static removeRequestFromHistory(request: IGraphApiCall) { 75 | const idx = AppComponent.requestHistory.indexOf(request); 76 | 77 | if (idx > -1) { 78 | AppComponent.requestHistory.splice(idx, 1); 79 | } else { 80 | return; 81 | } 82 | saveHistoryToLocalStorage(AppComponent.requestHistory); 83 | } 84 | 85 | public static setMessage(message: IMessage) { 86 | AppComponent.message = message; 87 | setTimeout(() => { GenericDialogComponent.showDialog(); }); 88 | } 89 | 90 | constructor(private GraphService: GraphService, private chRef: ChangeDetectorRef) { // tslint:disable-line 91 | super(); 92 | AppComponent._changeDetectionRef = chRef; 93 | } 94 | 95 | public ngAfterViewInit(): void { 96 | // When clicking on a pivot (request headers/body or response headers/body), notify ACE to update content 97 | if (typeof $ !== 'undefined') { 98 | $('api-explorer .ms-Pivot-link').on('click', () => { 99 | setTimeout(refreshAceEditorsContent, 0); 100 | }); 101 | } 102 | 103 | parseMetadata(this.GraphService, 'v1.0'); 104 | parseMetadata(this.GraphService, 'beta'); 105 | } 106 | 107 | public getLocalisedString(message: string): string { 108 | const g = new GraphExplorerComponent(); 109 | return g.getStr(message); 110 | } 111 | 112 | public ngOnInit() { 113 | for (const key in AppComponent.Options) { 114 | if (key in window) { 115 | AppComponent.Options[key] = window[key]; 116 | } 117 | } 118 | 119 | const hash = location.hash.substr(1); 120 | if (hash.includes('mode')) { 121 | const mode = 'canary'; 122 | localStorage.setItem('GRAPH_MODE', JSON.stringify(mode)); 123 | localStorage.setItem('GRAPH_URL', 'https://canary.graph.microsoft.com'); 124 | localLogout(); 125 | } 126 | 127 | AppComponent.Options.GraphVersions.push('Other'); 128 | 129 | initFabricComponents(); 130 | 131 | mwfAutoInit.ComponentFactory.create([{ 132 | component: mwfAutoInit.Drawer, 133 | }]); 134 | 135 | moment.locale(AppComponent.Options.Language); 136 | 137 | // Set explorer state that depends on configuration 138 | AppComponent.explorerValues.endpointUrl = getGraphUrl() 139 | + `/${(getParameterByName('version') || 'v1.0')}/${getParameterByName('request') || 'me/'}`; 140 | 141 | // Show the Microsoft Graph TOU when we load GE. 142 | AppComponent.messageBarContent = { 143 | // tslint:disable 144 | text: this.getLocalisedString('use the Microsoft Graph API') + ` 145 |

${this.getLocalisedString('Terms of use')}
147 | ${this.getLocalisedString('Microsoft Privacy Statement')}. 149 | `, 150 | // tslint:enable 151 | backgroundClass: 'ms-MessageBar--warning', 152 | icon: 'none', 153 | }; 154 | 155 | const authStatus = localStorage.getItem('status'); 156 | 157 | switch (authStatus) { 158 | case 'authenticated': 159 | AppComponent.explorerValues.authentication.status = 'authenticated'; 160 | break; 161 | case 'anonymous': 162 | AppComponent.explorerValues.authentication.status = 'anonymous'; 163 | break; 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/app/authentication/authentication.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { ChangeDetectorRef, Component } from '@angular/core'; 7 | import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; 8 | import { SeverityLevel } from '@microsoft/applicationinsights-web'; 9 | import { AuthResponse } from 'msal'; 10 | import { AppComponent } from '../app.component'; 11 | import { GraphService } from '../graph-service'; 12 | import { GraphExplorerComponent } from '../GraphExplorerComponent'; 13 | import { PermissionScopes } from '../scopes-dialog/scopes'; 14 | import { ScopesDialogComponent } from '../scopes-dialog/scopes-dialog.component'; 15 | import { telemetry } from '../telemetry/telemetry'; 16 | import { getGraphUrl } from '../util'; 17 | import { localLogout } from './auth'; 18 | import { acquireNewAccessToken, login, requiresInteraction } from './auth.service'; 19 | import { app } from './msal-user-agent'; 20 | 21 | @Component({ 22 | selector: 'authentication', 23 | styleUrls: ['./authentication.component.css'], 24 | templateUrl: './authentication.component.html', 25 | }) 26 | 27 | export class AuthenticationComponent extends GraphExplorerComponent { 28 | 29 | public authInfo = this.explorerValues.authentication; 30 | 31 | constructor( 32 | private sanitizer: DomSanitizer, private apiService: GraphService, 33 | private changeDetectorRef: ChangeDetectorRef) { 34 | super(); 35 | this.acquireTokenCallBack = this.acquireTokenCallBack.bind(this); 36 | this.acquireTokenErrorCallBack = this.acquireTokenErrorCallBack.bind(this); 37 | } 38 | 39 | public async ngOnInit() { 40 | // Register Callbacks for redirect flow 41 | app.handleRedirectCallback(this.acquireTokenErrorCallBack, this.acquireTokenCallBack); 42 | 43 | AppComponent.explorerValues.authentication.status = 'anonymous'; 44 | 45 | const prevVersion = localStorage.getItem('version'); 46 | const { appVersion } = (window as any); 47 | 48 | const hostname = window.location.hostname; 49 | 50 | /** 51 | * This forces a logout for users who do not have the version property in localstorage. 52 | * The version is set when they log in, so this will only happen once. 53 | */ 54 | if (hostname !== 'localhost' && prevVersion === null) { 55 | localStorage.clear(); 56 | } 57 | 58 | /** 59 | * Clear localStorage when the version of GraphExplorer changes. 60 | */ 61 | if (prevVersion && appVersion && appVersion !== prevVersion) { 62 | localStorage.clear(); 63 | } 64 | 65 | const account = app.getAccount(); 66 | const defaultScopes = AppComponent.Options.DefaultUserScopes; 67 | 68 | if (account) { 69 | AppComponent.explorerValues.authentication.status = 'authenticating'; 70 | await acquireNewAccessToken(app, defaultScopes) 71 | .then(this.acquireTokenCallBack) 72 | .then(this.acquireTokenErrorCallBack); 73 | } 74 | } 75 | public sanitize(url: string): SafeUrl { 76 | return this.sanitizer.bypassSecurityTrustUrl(url); 77 | } 78 | 79 | public async login() { 80 | AppComponent.explorerValues.authentication.status = 'authenticating'; 81 | 82 | /** 83 | * Setting the version here allows us to know which version of Graph Explorer the user authenticated with. 84 | */ 85 | const { appVersion } = (window as any); 86 | if (appVersion) { 87 | localStorage.setItem('version', appVersion); 88 | } 89 | 90 | await login(app).then(this.acquireTokenCallBack) 91 | .catch(this.acquireTokenErrorCallBack); 92 | } 93 | 94 | public logout() { 95 | localLogout(); 96 | app.logout(); 97 | } 98 | 99 | public getAuthenticationStatus() { 100 | return AppComponent.explorerValues.authentication.status; 101 | } 102 | 103 | public manageScopes() { 104 | ScopesDialogComponent.showDialog(); 105 | } 106 | 107 | public async setPermissions(response: AuthResponse) { 108 | const scopes = response.scopes; 109 | const scopesLowerCase = scopes.map((item) => { 110 | return item.toLowerCase(); 111 | }); 112 | scopesLowerCase.push('openid'); 113 | for (const scope of PermissionScopes) { 114 | // Scope.consented indicates that the user or admin has previously consented to the scope. 115 | scope.consented = scopesLowerCase.indexOf(scope.name.toLowerCase()) !== -1; 116 | } 117 | } 118 | 119 | private async displayUserProfile() { 120 | try { 121 | const userInfoUrl = `${getGraphUrl()}/v1.0/me`; 122 | const userPictureUrl = `${getGraphUrl()}/beta/me/photo/$value`; 123 | const userInfo = await this.apiService.performQuery('GET', userInfoUrl); 124 | const jsonUserInfo = userInfo.json(); 125 | 126 | AppComponent.explorerValues.authentication.user.displayName = jsonUserInfo.displayName; 127 | AppComponent.explorerValues.authentication.user.emailAddress 128 | = jsonUserInfo.mail || jsonUserInfo.userPrincipalName; 129 | 130 | try { 131 | const userPicture = await this.apiService.performQuery('GET_BINARY', userPictureUrl); 132 | const blob = new Blob([userPicture.arrayBuffer()], { type: 'image/jpeg' }); 133 | const imageUrl = window.URL.createObjectURL(blob); 134 | 135 | AppComponent.explorerValues.authentication.user.profileImageUrl = this.sanitize(imageUrl) as string; 136 | } catch (e) { 137 | AppComponent.explorerValues.authentication.user.profileImageUrl = null; 138 | } 139 | AppComponent.explorerValues.authentication.status = 'authenticated'; 140 | this.changeDetectorRef.detectChanges(); 141 | 142 | } catch (e) { 143 | localLogout(); 144 | } 145 | } 146 | 147 | private async acquireTokenCallBack(response) { 148 | if (response && response.tokenType === 'access_token') { 149 | AppComponent.explorerValues.authentication.status = 'authenticated'; 150 | this.displayUserProfile(); 151 | this.setPermissions(response); 152 | } else if (response && response.tokenType === 'id_token') { 153 | await acquireNewAccessToken(app) 154 | .then(this.acquireTokenCallBack).catch(this.acquireTokenErrorCallBack); 155 | } 156 | } 157 | 158 | private acquireTokenErrorCallBack(error: any): void { 159 | AppComponent.explorerValues.authentication.status = 'anonymous'; 160 | 161 | if (error) { 162 | telemetry.trackException(error, SeverityLevel.Critical); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Graph Explorer 2 | [![Build Status](https://travis-ci.org/microsoftgraph/microsoft-graph-explorer.svg?branch=master)](https://travis-ci.org/microsoftgraph/microsoft-graph-explorer) 3 | 4 | The [Microsoft Graph Explorer](https://developer.microsoft.com/graph/graph-explorer) lets developers quickly navigate and test API endpoints. 5 | 6 | The Graph Explorer is written in [TypeScript](https://www.typescriptlang.org/) and powered by: 7 | * [Angular 4](https://angular.io/) 8 | * [Office Fabric](https://dev.office.com/fabric) 9 | * [Microsoft Web Framework](https://getmwf.com/) 10 | 11 | ## Running the explorer locally 12 | 13 | * `npm install` to install project dependencies. `npm` is installed by default with [Node.js](https://nodejs.org/). 14 | * `npm start` starts the TypeScript compiler in watch mode and the local server. It should open your browser automatically with the Graph Explorer at [http://localhost:3000/](http://localhost:3000). 15 | 16 | #### Enabling authentication with your own credentials 17 | * You'll need to register an app on [apps.dev.microsoft.com](https://apps.dev.microsoft.com) to configure the login page for your local Graph Explorer. Under `Platforms` click `Add Platform` and select Web. `Allow Implicit Flow` should be checked and set `http://localhost:3000` as the redirect URL. You don't need a client secret since the explorer is a single page application. Select the delegated permissions that you'll want to use in your local Graph Explorer. 18 | * Rename `secrets.sample.js` to `secrets.js` in the project root and insert your client ID. 19 | 20 | ## Other commands 21 | * `npm test` to run tests from the command line for scenarios like parsing metadata and functional explorer tests. 22 | * `npm run import:loc-strings` combines all the loc files in `translation_files/` to `scripts/loc_strings.ts` 23 | * `npm run build:prod` to build the minified explorer for production use. 24 | 25 | ## Contributing 26 | Please see the [contributing guidelines](CONTRIBUTING.md). 27 | 28 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 29 | 30 | #### Sample queries 31 | 32 | We want the explorer to have rich samples for calling all APIs in the Microsoft Graph. Choose the most relevant queries that demonstrate your feature. GET samples are the only queries that will work against our demo tenant. There are a few things that you need to check before you can add a sample query: 33 | - [ ] Does your sample query use [scopes]( https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-scopes) that are available via the [Azure AD v2.0 authentication]( https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-appmodel-v2-overview) endpoint? You’ll need to confirm this before you can add your sample. 34 | - [ ] Does Graph Explorer already have the scopes that support your query? If not, you’ll need to add the scopes. See the [permissions](#permissions) section for how you can add your scopes. 35 | - [ ] Does documentation exist for functionality described in your sample query? [Create](https://github.com/microsoftgraph/microsoft-graph-docs) and publish the documentation before you add your sample. 36 | - [ ] Is the sample query functionality available on either the beta or v1.0 Graph endpoints? Confirm that the functionality is available on at least of one of these endpoints before submitting your sample query. 37 | - [ ] Are you certain that the functionality in the sample query will not change? We don’t want to have outdated samples, so only add samples when you are confident that the API won’t change. 38 | - [ ] Does the Graph metadata properly describe the functionality in the sample query? Confirm that either the [v1.0](https://graph.microsoft.com/v1.0/$metadata) or [beta]( https://graph.microsoft.com/beta/$metadata) metadata describes your functionality. 39 | 40 | When your answer is yes to all of the questions, you are now ready to add your sample query. Before you do that, we need to check whether you require sample data. 41 | - [ ] Do your sample queries require data to be set up on the sample tenant? Contact the Graph Explorer maintainers to request sample data setup. 42 | - [ ] Do your sample queries require placeholder data? If so, you’ll need to update [tokens.ts](./src/app/tokens.ts) with your placeholder data. Some sample queries have ids or other string constants that are different for authenticated users and the sample tenant. These tokens are maintained in tokens.ts. Token documentation can be found in the Token interface located in [base.ts](./src/app/base.ts). 43 | 44 | Sample queries are added to [sample-queries.csv](./sample-queries.csv). You’ll add your sample query to this file. Make sure you fill out all of the fields that are applicable to your query. Run `npm run import:samples` after you’ve added your sample query. This will convert the samples in the CSV file to structured objects in src/app/get-queries. 45 | 46 | Once you've added your sample queries, you'll need to add your query titles and category name to the en-US loc string files found in the [translation](./translation_files) directory. We'll localize and import the loc strings later. 47 | 48 | Next, you need to run Graph Explorer on your development computer to verify that the samples are working as you expect. Go to [Running the explorer locally]( https://github.com/microsoftgraph/microsoft-graph-explorer#running-the-explorer-locally) to learn how to run Graph Explorer. Confirm that your samples work as expected with a signed in user account. 49 | 50 | We now need to confirm that your samples will work against the demo tenant used when the user is not logged in. Run `npm test` to test all of the **GET** queries against the demo tenant. 51 | 52 | Now that you have a working sample query, build the Graph Explorer so that your changes are available to be staged. Run `npm run build:prod`. 53 | 54 | You are now ready to open your pull request to submit your changes. All GET sample queries are tested when you push changes or create a pull request. These samples all must pass before we can review your changes. 55 | 56 | **Note**: If you see Unexpected token T in JSON at position 0 as an error when you run npm test, when you push your changes, or when you open a pull request, then you may have an unexpected space in your sample. If you get a timeout error, restart the Travis CI job. This type of error is often intermittent. 57 | 58 | #### Permissions 59 | 60 | Permissions like `Mail.Read` are listed in [scopes.ts](src/app/scopes-dialog/scopes.ts) and each permission has a few properties, like its name and description. 61 | 62 | ```javascript 63 | { 64 | name: "Calendars.ReadWrite", 65 | description: "Have full access to user calendars", 66 | longDescription: "Allows the app to create, read, update, and delete events in user calendars.", 67 | preview: false, 68 | admin: false 69 | } 70 | ``` 71 | Edits to this file can be made directly from Github.com so you don't even have to clone the project to add a new permission. You can also see [a merged pull request](https://github.com/microsoftgraph/microsoft-graph-explorer/pull/48) for adding the `Reports.Read.All` permission. 72 | 73 | ## Known issues 74 | * You cannot remove permissions by using the Graph Explorer UI. You will need to [remove the application consent](http://shawntabrizi.com/aad/revoking-consent-azure-active-directory-applications/) and then re-consent to remove permissions. I know, this is far from a good experience. 75 | 76 | ## Additional resources 77 | * [Microsoft Graph website](https://graph.microsoft.io) 78 | * [Office Dev Center](http://dev.office.com/) 79 | * [Graph Explorer releases](https://github.com/microsoftgraph/microsoft-graph-explorer/releases) 80 | 81 | ## Copyright 82 | Copyright (c) 2017 Microsoft. All rights reserved. 83 | -------------------------------------------------------------------------------- /src/app/base.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { isAuthenticated } from './authentication/auth'; 7 | import { IHarFormat } from './history/har/IHarFormat'; 8 | import { Tokens } from './tokens'; 9 | 10 | export interface IExplorerOptions { 11 | AuthUrl?: string; 12 | ClientId?: string; 13 | Language?: string; 14 | RedirectUrl?: string; 15 | DefaultUserScopes?: string[]; 16 | GraphVersions?: string[]; 17 | PathToBuildDir: string; 18 | } 19 | 20 | export let Methods: RequestType[] = [ 21 | 'GET', 22 | 'POST', 23 | 'PUT', 24 | 'PATCH', 25 | 'DELETE', 26 | ]; 27 | 28 | export type GraphApiVersion = 'v1.0' | 'beta'; 29 | export let GraphApiVersions: GraphApiVersion[] = ['v1.0', 'beta']; 30 | 31 | export type AuthenticationStatus = 'anonymous' | 'authenticating' | 'authenticated'; 32 | 33 | export type RequestType = 'GET' | 'PUT' | 'POST' | 'GET_BINARY' | 'POST' | 'PATCH' | 'DELETE'; 34 | 35 | /*** 36 | * Represents a scope. 37 | */ 38 | export interface IPermissionScope { 39 | /** 40 | * The scope name. 41 | */ 42 | name: string; 43 | /** 44 | * A short description of the scope. 45 | */ 46 | description: string; 47 | /** 48 | * A long description of the scope. 49 | */ 50 | longDescription: string; 51 | /** 52 | * Specifies whether the scope is currently in preview. 53 | */ 54 | preview: boolean; 55 | /** 56 | * Specifies whether the property is only consent-able via admin consent. 57 | */ 58 | admin: boolean; 59 | /** 60 | * Specifies whether the user has already consented to the scope. 61 | */ 62 | consented?: boolean; 63 | /** 64 | * Specifies whether the user wants to request this scope. Used in the scopes 65 | * dialog for checking/unchecking before scope is actually enabled in the token. 66 | */ 67 | requested?: boolean; 68 | } 69 | 70 | export interface IGraphApiCall { 71 | statusCode?: number; 72 | duration?: number; 73 | method?: RequestType; 74 | humanName?: string; 75 | requestUrl?: string; 76 | postBody?: string; 77 | headers?: IGraphRequestHeader[]; 78 | 79 | requestSentAt?: Date; 80 | relativeDate?: string; 81 | har?: string; 82 | } 83 | 84 | export interface ISampleQuery extends IGraphApiCall { 85 | docLink?: string; 86 | AAD?: boolean; 87 | skipTest?: boolean; 88 | MSA?: boolean; 89 | category: string; 90 | tip?: string; 91 | } 92 | 93 | export interface ISampleQueryCategory { 94 | title: string; 95 | enabled?: boolean; 96 | queries?: ISampleQuery[]; 97 | } 98 | 99 | export interface IExplorerValues { 100 | selectedOption?: RequestType; 101 | selectedVersion?: GraphApiVersion; 102 | endpointUrl?: string; 103 | authentication?: { 104 | status?: AuthenticationStatus 105 | user?: { 106 | displayName?: string 107 | emailAddress?: string 108 | profileImageUrl?: string, 109 | }, 110 | }; 111 | showImage?: boolean; 112 | requestInProgress?: boolean; 113 | headers?: IGraphRequestHeader[]; 114 | postBody?: string; 115 | } 116 | 117 | export interface IGraphRequestHeader { 118 | name: string; 119 | value: string; 120 | enabled?: boolean; 121 | readonly?: boolean; 122 | } 123 | 124 | export interface IAutoCompleteItem { 125 | url: string; 126 | fullUrl: string; 127 | } 128 | 129 | export const CommonHeaders = [ 130 | 'Accept', 131 | 'Accept-Charset', 132 | 'Accept-Encoding', 133 | 'Accept-Language', 134 | 'Accept-Datetime', 135 | 'Authorization', 136 | 'Cache-Control', 137 | 'Connection', 138 | 'Cookie', 139 | 'Content-Length', 140 | 'Content-MD5', 141 | 'Content-Type', 142 | 'Date', 143 | 'Expect', 144 | 'Forwarded', 145 | 'From', 146 | 'Host', 147 | 'If-Match', 148 | 'If-Modified-Since', 149 | 'If-None-Match', 150 | 'If-Range', 151 | 'If-Unmodified-Since', 152 | 'Max-Forwards', 153 | 'Origin', 154 | 'Pragma', 155 | 'Proxy-Authorization', 156 | 'Range', 157 | 'User-Agent', 158 | 'Upgrade', 159 | 'Via', 160 | 'Warning', 161 | ]; 162 | 163 | /** 164 | * Tokens are used as placeholder values in sample queries to cover many scenarios: 165 | * - ID tokens for sample tenant nodes like user IDs, file IDs and other string constants 166 | * - Tokens that must be determined at runtime like the current date 167 | * - Tokens that are determined from the authenticated users session 168 | * - Tokens can be in the POST body or part of the URL 169 | * 170 | * The token fields are split into default, demo and authenticated. If neither the demo or 171 | * auth values are supplied, the token falls back to the default value. 172 | * 173 | * Tokens are maintained in tokens.ts. 174 | */ 175 | 176 | export interface IToken { 177 | placeholder: string; 178 | 179 | // Base defaults to replace the placeholder with. Not used if any of the below are defined 180 | defaultValue?: string; 181 | defaultValueFn?: Function; 182 | 183 | // When the user is not authenticated, use these values for the demo tenant 184 | demoTenantValue?: string; 185 | demoTenantValueFn?: Function; 186 | 187 | // When the user is authenticated with MSA or AAD, replace token with these values 188 | authenticatedUserValue?: string; 189 | authenticatedUserValueFn?: Function; 190 | } 191 | 192 | /* 193 | * Given a token, go through each of the possible replacement scenarios and find which value to 194 | * replace the token with. 195 | * Order: Authenticated user values, demo tenant replacament values, default replacement values. 196 | */ 197 | function getTokenSubstituteValue(token: IToken) { 198 | const priorityOrder = []; // Desc 199 | 200 | if (isAuthenticated()) { 201 | priorityOrder.push(token.authenticatedUserValueFn); 202 | priorityOrder.push(token.authenticatedUserValue); 203 | } else { 204 | priorityOrder.push(token.demoTenantValueFn); 205 | priorityOrder.push(token.demoTenantValue); 206 | } 207 | 208 | priorityOrder.push(token.defaultValueFn); 209 | priorityOrder.push(token.defaultValue); 210 | 211 | for (const tokenVal of priorityOrder) { 212 | if (!tokenVal) { 213 | continue; 214 | } 215 | if (typeof tokenVal === 'string') { 216 | return tokenVal; 217 | } else if (typeof tokenVal === 'function') { 218 | return tokenVal(); 219 | } 220 | } 221 | 222 | } 223 | 224 | /** 225 | * Given a query, replace all tokens in the request URL and the POST body with thier 226 | * values. When a token is found, use getTokenSubstituteValue() to find the right 227 | * value to substitute based on the session. 228 | */ 229 | export function substituteTokens(query: ISampleQuery) { 230 | type QueryFields = keyof IGraphApiCall; 231 | 232 | for (const token of Tokens) { 233 | const queryFieldsToCheck: QueryFields[] = ['requestUrl', 'postBody']; 234 | 235 | for (const queryField of queryFieldsToCheck) { 236 | if (!query[queryField]) { // If the sample doesn't have a post body, don't search for tokens in it 237 | continue; 238 | } 239 | 240 | if ((query[queryField] as string).indexOf(`{${token.placeholder}}`) !== -1) { 241 | const substitutedValue = getTokenSubstituteValue(token); 242 | if (!substitutedValue) { 243 | continue; 244 | } 245 | query[queryField] = (query[queryField] as string).replace(`{${token.placeholder}}`, substitutedValue); 246 | } 247 | } 248 | } 249 | } 250 | 251 | export interface IMessage { 252 | title: string; 253 | body: string; 254 | } 255 | 256 | export interface IMessageBarContent { 257 | icon: string; 258 | backgroundClass: string; 259 | text: string; 260 | } 261 | 262 | export let AllowedGraphDomains = [ 263 | 'https://graph.microsoft.com', 264 | 'https://canary.graph.microsoft.com', 265 | 'https://microsoftgraph.chinacloudapi.cn', 266 | ]; 267 | -------------------------------------------------------------------------------- /src/app/scopes-dialog/scopes-dialog.component.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. 3 | // See License in the project root for license information. 4 | // ------------------------------------------------------------------------------ 5 | 6 | import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; 7 | import { acquireNewAccessToken, getLoginType } from '../authentication/auth.service'; 8 | import { app } from '../authentication/msal-user-agent'; 9 | import { IPermissionScope } from '../base'; 10 | import { GraphExplorerComponent } from '../GraphExplorerComponent'; 11 | import { PermissionScopes } from './scopes'; 12 | 13 | declare let fabric; 14 | declare let mwfAutoInit; 15 | 16 | @Component({ 17 | selector: 'scopes-dialog', 18 | styleUrls: ['./scopes-dialog.component.css'], 19 | templateUrl: './scopes-dialog.component.html', 20 | }) 21 | export class ScopesDialogComponent extends GraphExplorerComponent implements AfterViewInit { 22 | public static fabricDialog: any; 23 | 24 | public static showDialog() { // tslint:disable-line 25 | 26 | const scopesDialog = document.querySelector('#scopes-dialog'); 27 | ScopesDialogComponent.fabricDialog = new fabric.Dialog(scopesDialog); 28 | ScopesDialogComponent.fabricDialog.open(); 29 | 30 | mwfAutoInit.ComponentFactory.create([{ 31 | component: mwfAutoInit.Checkbox, 32 | }]); 33 | 34 | // We are explicitly focusing on the close icon button here despite setting the autofocus property. 35 | // This is because Edge does not give this element focus even with the autofocus property set. 36 | // See: https://stackoverflow.com/questions/51867504/edge-how-to-make-autofocus-work-with-refresh-button 37 | (scopesDialog.childNodes[2] as any).focus(); 38 | } 39 | 40 | public static closeDialog() { 41 | ScopesDialogComponent.fabricDialog.close(); 42 | (document.querySelector('#modify-permissions') as any).focus(); 43 | } 44 | 45 | public scopes: IPermissionScope[] = PermissionScopes; 46 | 47 | /** 48 | * Angular doesn't support access of static properties in .html file. This creates an instance variable 49 | * which can be accessed from the template files. The variable is used as a proxy for accessing static 50 | * properties of this class. 51 | */ 52 | public ScopesDialogComponent = ScopesDialogComponent; 53 | 54 | /** 55 | * Specifies whether we have any admin scopes selected in the scopes-dialog UI. 56 | * If this value is true, we will show an alert that admin consent will be required 57 | * to use this scope. 58 | */ 59 | public hasSelectedAdminScope: boolean = false; 60 | 61 | /** 62 | * A container to track which admin scopes have been selected in the UI. 63 | */ 64 | public selectedTargetedAdminScopes: IPermissionScope[] = []; 65 | 66 | // Use this to get a handle on the scopes-list-table-container element. 67 | @ViewChild('scopesListTableContainer') public scopesTableList: ElementRef; 68 | 69 | // Contains the scopes list table height. The maximum height value is 451px. 70 | public scopesListTableHeight: string; 71 | 72 | // Flags for changing the scopes list table height. 73 | public hasChangedScopeListHeight: boolean = false; 74 | public hasRequestedAdminConsent: boolean = false; 75 | 76 | constructor() { 77 | super(); 78 | } 79 | // Updates the style.height of the scopes-list-table-container element. 80 | public getScopesListTableHeight(): string { 81 | this.scopesListTableHeight = window.getComputedStyle(this.scopesTableList.nativeElement, null) 82 | .getPropertyValue('height'); 83 | return this.scopesListTableHeight; 84 | } 85 | 86 | public getScopeLabel(scopeName: string): string { 87 | return scopeName + ' scope'; 88 | } 89 | 90 | public ngAfterViewInit(): void { 91 | this.sortScopesList(); 92 | (window as any).launchPermissionsDialog = ScopesDialogComponent.showDialog; 93 | this.scopesListTableHeight = window 94 | .getComputedStyle(this.scopesTableList.nativeElement, null).getPropertyValue('height'); 95 | } 96 | 97 | public sortScopesList(): void { 98 | PermissionScopes.sort((a, b) => { 99 | const scopeNameA = a.name.toUpperCase(); 100 | const scopeNameB = b.name.toUpperCase(); 101 | return (scopeNameA < scopeNameB) ? -1 : (scopeNameA > scopeNameB) ? 1 : 0; 102 | }); 103 | } 104 | 105 | /* 106 | * Indicates whether the scope list has been changed. 107 | * @returns {boolean} A value of true indicates that a scope has been added or removed from the scope list. 108 | */ 109 | public scopeListIsDirty(): boolean { 110 | 111 | // Determine whether the scope list has changed. The scope list has changed if isDirty = true. 112 | const isDirty = PermissionScopes.filter((s) => s.requested === true).length > 0; 113 | 114 | // Reduce the size of the scopes table list by the size of the message bar. We only want to make this 115 | // Change the first time that the selected scope list is changed. 116 | if (isDirty && !this.hasChangedScopeListHeight) { 117 | 118 | // Convert the table height from string to number. 119 | const currentHeight: number = Number(this.scopesListTableHeight.replace(/px/, '')).valueOf(); 120 | 121 | // Update the table height based on the height of the message bar that's displayed when the scope list is dirty. 122 | // This should keep the buttons from moving below the viewport as long as the component is showing in the viewport 123 | const updatedHeight: number = currentHeight - 60; 124 | 125 | // Convert the updated height to a string and set the scopes list table height style. 126 | this.scopesTableList.nativeElement.style.height = updatedHeight.toString() + 'px'; 127 | 128 | // We only want to adjust the height one time after making a change to the selected scope list. 129 | this.hasChangedScopeListHeight = true; 130 | } 131 | 132 | return isDirty; 133 | } 134 | 135 | /* 136 | * Indicates whether an admin scope has been selected. 137 | * @returns {boolean} A value of true indicates that a scope that requires admin consent has been selected. 138 | */ 139 | public requestingAdminScopes(): boolean { 140 | 141 | // Determine whether a scope that requires admin consent has been requested. An admin consent scope has been 142 | // Selected if isDirty = true. 143 | const isDirty = PermissionScopes.filter((s) => s.admin && s.requested).length > 0; 144 | 145 | // Reduce the size of the scopes table list by the size of the message bar. We only want to make this 146 | // Change the first time that the selected scope list is changed. 147 | if (isDirty && !this.hasRequestedAdminConsent) { 148 | // Convert the table height from string to number. 149 | const currentHeight: number = Number(this.scopesListTableHeight.replace(/px/, '')).valueOf(); 150 | 151 | /* 152 | Update the table height based on the height of the message bar that's displayed when an admin consent scope is 153 | selected. 154 | This should keep the buttons from moving below the viewport as long as the component is showing in the viewport. 155 | */ 156 | const updatedHeight: number = currentHeight - 135; 157 | 158 | // Convert the updated height to a string and set the scopes list table height style. 159 | this.scopesTableList.nativeElement.style.height = updatedHeight.toString() + 'px'; 160 | 161 | // We only want to adjust the height one time after making a change to the selected scope list. 162 | this.hasRequestedAdminConsent = true; 163 | } 164 | 165 | return isDirty; 166 | } 167 | 168 | /** 169 | * Toggles whether the scope will be requested. This occurs in the Modify Permissions UI by selecting a checkbox. 170 | * This will be used to determine whether we will request consent for this scope. 171 | * @param scope The scope to toggle its enabled state. 172 | */ 173 | public toggleRequestScope(scope: IPermissionScope) { 174 | scope.requested = !scope.requested; 175 | 176 | // Track whether we have any admin scopes selected in the UI to be enabled for the user. 177 | if (scope.admin && scope.requested) { 178 | this.selectedTargetedAdminScopes.push(scope); 179 | this.hasSelectedAdminScope = true; 180 | } else if (scope.admin && !scope.requested) { 181 | this.selectedTargetedAdminScopes = this.selectedTargetedAdminScopes.filter((e) => e !== scope); 182 | if (this.selectedTargetedAdminScopes.length === 0) { 183 | this.hasSelectedAdminScope = false; 184 | } 185 | } 186 | } 187 | 188 | public async getNewAccessToken() { 189 | const selectedScopes = PermissionScopes.filter((scope) => scope.requested && !scope.consented) 190 | .map((scope) => scope.name); 191 | await acquireNewAccessToken(app, selectedScopes); 192 | } 193 | 194 | public focusOnFirstElement(firstElement: Element) { 195 | (firstElement as any).focus(); 196 | } 197 | 198 | public focusOnLastElement(event: any, lastElement: Element) { 199 | if (event.shiftKey && event.keyCode === 9) { 200 | (lastElement as any).focus(); 201 | } 202 | } 203 | 204 | public getTabIndex(scope) { 205 | // Consented scopes appear as disabled elements. 206 | // Users should not be able to tab through them. 207 | const isDisabled = scope.consented; 208 | return isDisabled ? '-1' : '0'; 209 | } 210 | } 211 | --------------------------------------------------------------------------------