├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── assets ├── add-to-page.png ├── api-tester-demo.gif └── trust-solution.png ├── config ├── copy-assets.json ├── write-manifests.json ├── deploy-azure-storage.json ├── serve.json ├── package-solution.json ├── config.json └── tslint.json ├── src └── webparts │ └── restTester │ ├── components │ ├── IRestTesterProps.ts │ ├── known-apis.json │ ├── ApiSuggestions.tsx │ ├── HeadersInput.tsx │ ├── RestTester.module.scss │ ├── SnippetBuilder.tsx │ ├── ResponseInfo.tsx │ └── RestTester.tsx │ ├── loc │ ├── en-us.js │ └── mystrings.d.ts │ ├── RestTesterWebPart.manifest.json │ └── RestTesterWebPart.ts ├── .yo-rc.json ├── gulpfile.js ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── CHANGELOG.md ├── update-changelog.js ├── LICENSE ├── package.json ├── changelog.json └── README.md /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "msjsdiag.debugger-for-chrome" 4 | ] 5 | } -------------------------------------------------------------------------------- /assets/add-to-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/estruyf/spfx-rest-api-tester/HEAD/assets/add-to-page.png -------------------------------------------------------------------------------- /assets/api-tester-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/estruyf/spfx-rest-api-tester/HEAD/assets/api-tester-demo.gif -------------------------------------------------------------------------------- /assets/trust-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/estruyf/spfx-rest-api-tester/HEAD/assets/trust-solution.png -------------------------------------------------------------------------------- /config/copy-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json", 3 | "deployCdnPath": "temp/deploy" 4 | } 5 | -------------------------------------------------------------------------------- /config/write-manifests.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json", 3 | "cdnBasePath": "" 4 | } -------------------------------------------------------------------------------- /src/webparts/restTester/components/IRestTesterProps.ts: -------------------------------------------------------------------------------- 1 | import {WebPartContext} from "@microsoft/sp-webpart-base"; 2 | 3 | export interface IRestTesterProps { 4 | context: WebPartContext; 5 | } 6 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@microsoft/generator-sharepoint": { 3 | "version": "1.4.1", 4 | "libraryName": "rest-tester", 5 | "libraryId": "96c4456c-cae4-47e4-ad18-6597045a2391", 6 | "environment": "spo" 7 | } 8 | } -------------------------------------------------------------------------------- /src/webparts/restTester/loc/en-us.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | return { 3 | "PropertyPaneDescription": "Description", 4 | "BasicGroupName": "Group Name", 5 | "DescriptionFieldLabel": "Description Field" 6 | } 7 | }); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const build = require('@microsoft/sp-build-web'); 5 | build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); 6 | 7 | build.initialize(gulp); 8 | -------------------------------------------------------------------------------- /config/deploy-azure-storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", 3 | "workingDir": "./temp/deploy/", 4 | "account": "", 5 | "container": "rest-tester", 6 | "accessKey": "" 7 | } -------------------------------------------------------------------------------- /config/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json", 3 | "port": 4321, 4 | "https": true, 5 | "initialPage": "https://localhost:5432/workbench", 6 | "api": { 7 | "port": 5432, 8 | "entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/webparts/restTester/loc/mystrings.d.ts: -------------------------------------------------------------------------------- 1 | declare interface IRestTesterWebPartStrings { 2 | PropertyPaneDescription: string; 3 | BasicGroupName: string; 4 | DescriptionFieldLabel: string; 5 | } 6 | 7 | declare module 'RestTesterWebPartStrings' { 8 | const strings: IRestTesterWebPartStrings; 9 | export = strings; 10 | } 11 | -------------------------------------------------------------------------------- /config/package-solution.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json", 3 | "solution": { 4 | "name": "estruyf-rest-api-tester", 5 | "id": "96c4456c-cae4-47e4-ad18-6597045a2392", 6 | "version": "1.0.0.0", 7 | "includeClientSideAssets": true, 8 | "skipFeatureDeployment": true 9 | }, 10 | "paths": { 11 | "zippedPackage": "solution/estruyf-rest-api-tester.sppkg" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // Configure glob patterns for excluding files and folders in the file explorer. 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.DS_Store": true, 7 | "**/bower_components": true, 8 | "**/coverage": true, 9 | "**/lib-amd": true, 10 | "src/**/*.scss.ts": true 11 | }, 12 | "typescript.tsdk": ".\\node_modules\\typescript\\lib" 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | node_modules 8 | 9 | # Build generated files 10 | dist 11 | lib 12 | solution 13 | temp 14 | *.sppkg 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # OSX 20 | .DS_Store 21 | 22 | # Visual Studio files 23 | .ntvs_analysis.dat 24 | .vs 25 | bin 26 | obj 27 | 28 | # Resx Generated Code 29 | *.resx.ts 30 | 31 | # Styles Generated Code 32 | *.scss.ts 33 | 34 | sample.txt 35 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json", 3 | "version": "2.0", 4 | "bundles": { 5 | "rest-tester-web-part": { 6 | "components": [ 7 | { 8 | "entrypoint": "./lib/webparts/restTester/RestTesterWebPart.js", 9 | "manifest": "./src/webparts/restTester/RestTesterWebPart.manifest.json" 10 | } 11 | ] 12 | } 13 | }, 14 | "externals": {}, 15 | "localizedResources": { 16 | "RestTesterWebPartStrings": "lib/webparts/restTester/loc/{locale}.js" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # we recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [{package,bower}.json] 24 | indent_style = space 25 | indent_size = 2 -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "typeRoots": [ 12 | "./node_modules/@types", 13 | "./node_modules/@microsoft" 14 | ], 15 | "types": [ 16 | "es6-promise", 17 | "webpack-env" 18 | ], 19 | "lib": [ 20 | "es5", 21 | "dom", 22 | "es2015.collection" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/webparts/restTester/RestTesterWebPart.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", 3 | "id": "97741cd2-a5d0-4f21-81d2-865523a3f0d4", 4 | "alias": "estruyf-apitester", 5 | "componentType": "WebPart", 6 | "version": "*", 7 | "manifestVersion": 2, 8 | "requiresCustomScript": false, 9 | "preconfiguredEntries": [{ 10 | "groupId": "5c03119e-3074-46fd-976b-c60198311f71", 11 | "group": { "default": "Other" }, 12 | "title": { "default": "REST API Tester" }, 13 | "description": { "default": "RestTester description" }, 14 | "officeFabricIconFontName": "TestBeaker", 15 | "properties": { 16 | "description": "REST API Tester" 17 | } 18 | }] 19 | } 20 | -------------------------------------------------------------------------------- /src/webparts/restTester/components/known-apis.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": [ 3 | { "method": "GET", "url": "_api/web" }, 4 | { "method": "GET", "url": "_api/web/Lists" }, 5 | { "method": "GET", "url": "_api/web/Lists(guid'{listId}')" }, 6 | { "method": "GET", "url": "_api/web/Lists/GetByTitle('')" }, 7 | { "method": "GET", "url": "_api/web/Lists(guid'{listId}')/Items" }, 8 | { "method": "GET", "url": "_api/web/Lists/GetByTitle('')/Items" }, 9 | { "method": "GET", "url": "_api/web/lists(guid'{listId}')/subscriptions" }, 10 | { "method": "GET", "url": "_api/web/lists(guid'{listId}')/getchanges" }, 11 | { "method": "GET", "url": "_api/web/getuserbyid('{userId}')" }, 12 | { "method": "POST", "url": "_api/site/openWebById('{webId}')" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog - REST API Tester 2 | 3 | ## [1.0.0] - 2018-03-29 4 | 5 | ### Added 6 | 7 | - First major release with automated release 8 | 9 | ## [0.0.4] - 2018-03-28 10 | 11 | ### Added 12 | 13 | - API URL Suggestions 14 | - New request methods added: PUT, PATCH, DELETE, HEAD 15 | - New tokens: siteId, webId and userId 16 | - Response message bar with status code and called URL 17 | 18 | ## [0.0.3] 19 | 20 | ### Added 21 | 22 | - Request header support 23 | - Code snippet for SPFx solution with the performed API call 24 | - Search enabled in code editor 25 | - Code/line wrapping 26 | 27 | ## [0.0.2] 28 | 29 | ### Added 30 | 31 | - Response status code is shown with each request 32 | - Automatic conversion from JSON to TS interface 33 | 34 | ## [0.0.1] 35 | 36 | ### Added 37 | 38 | - Initial BETA version 39 | -------------------------------------------------------------------------------- /update-changelog.js: -------------------------------------------------------------------------------- 1 | const changelog = require('./changelog.json'); 2 | const fs = require('fs'); 3 | 4 | if (changelog.versions && changelog.versions.length > 0) { 5 | const markdown = []; 6 | 7 | markdown.push(`# Changelog - REST API Tester`); 8 | markdown.push(``); 9 | 10 | changelog.versions.forEach(v => { 11 | markdown.push(`## [${v.version}] ${v.date ? `- ${v.date}` : ''}`); 12 | markdown.push(``); 13 | 14 | if (v.changes) { 15 | for (const key in v.changes) { 16 | const typeChange = v.changes[key]; 17 | if (typeChange.length > 0) { 18 | markdown.push(`### ${key.charAt(0).toUpperCase() + key.slice(1)}`); 19 | markdown.push(``); 20 | typeChange.forEach(msg => { 21 | markdown.push(`- ${msg}`); 22 | }); 23 | markdown.push(``); 24 | } 25 | } 26 | } 27 | }); 28 | 29 | if (markdown.length > 2) { 30 | fs.writeFileSync('CHANGELOG.md', markdown.join('\n')); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Elio Struyf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-tester", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": ">=0.10.0" 7 | }, 8 | "scripts": { 9 | "build": "gulp clean && gulp bundle && gulp package-solution", 10 | "build:prod": "gulp clean && gulp bundle --ship && gulp package-solution --ship && npm run changelog", 11 | "changelog": "node update-changelog.js" 12 | }, 13 | "dependencies": { 14 | "@microsoft/sp-core-library": "~1.4.1", 15 | "@microsoft/sp-lodash-subset": "~1.4.1", 16 | "@microsoft/sp-office-ui-fabric-core": "~1.4.1", 17 | "@microsoft/sp-webpart-base": "~1.4.1", 18 | "@types/js-beautify": "0.0.31", 19 | "@types/react": "15.6.6", 20 | "@types/react-dom": "15.5.6", 21 | "@types/webpack-env": ">=1.12.1 <1.14.0", 22 | "js-beautify": "1.7.5", 23 | "json-to-ts": "1.5.4", 24 | "react": "15.6.2", 25 | "react-ace": "5.9.0", 26 | "react-dom": "15.6.2" 27 | }, 28 | "devDependencies": { 29 | "@microsoft/sp-build-web": "~1.4.1", 30 | "@microsoft/sp-module-interfaces": "~1.4.1", 31 | "@microsoft/sp-webpart-workbench": "~1.4.1", 32 | "gulp": "~3.9.1", 33 | "@types/chai": ">=3.4.34 <3.6.0", 34 | "@types/mocha": ">=2.2.33 <2.6.0", 35 | "ajv": "~5.2.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | * Install Chrome Debugger Extension for Visual Studio Code to debug your components with the 4 | * Chrome browser: https://aka.ms/spfx-debugger-extensions 5 | */ 6 | "version": "0.2.0", 7 | "configurations": [{ 8 | "name": "Local workbench", 9 | "type": "chrome", 10 | "request": "launch", 11 | "url": "https://localhost:4321/temp/workbench.html", 12 | "webRoot": "${workspaceRoot}", 13 | "sourceMaps": true, 14 | "sourceMapPathOverrides": { 15 | "webpack:///../../../src/*": "${webRoot}/src/*", 16 | "webpack:///../../../../src/*": "${webRoot}/src/*", 17 | "webpack:///../../../../../src/*": "${webRoot}/src/*" 18 | }, 19 | "runtimeArgs": [ 20 | "--remote-debugging-port=9222" 21 | ] 22 | }, 23 | { 24 | "name": "Hosted workbench", 25 | "type": "chrome", 26 | "request": "launch", 27 | "url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx", 28 | "webRoot": "${workspaceRoot}", 29 | "sourceMaps": true, 30 | "sourceMapPathOverrides": { 31 | "webpack:///../../../src/*": "${webRoot}/src/*", 32 | "webpack:///../../../../src/*": "${webRoot}/src/*", 33 | "webpack:///../../../../../src/*": "${webRoot}/src/*" 34 | }, 35 | "runtimeArgs": [ 36 | "--remote-debugging-port=9222", 37 | "-incognito" 38 | ] 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /changelog.json: -------------------------------------------------------------------------------- 1 | { 2 | "versions": [ 3 | { 4 | "version": "1.0.0", 5 | "date": "2018-03-29", 6 | "changes": { 7 | "added": [ 8 | "First major release with automated release" 9 | ], 10 | "changed": [], 11 | "removed": [], 12 | "fixed": [] 13 | } 14 | }, 15 | { 16 | "version": "0.0.4", 17 | "date": "2018-03-28", 18 | "changes": { 19 | "added": [ 20 | "API URL Suggestions", 21 | "New request methods added: PUT, PATCH, DELETE, HEAD", 22 | "New tokens: siteId, webId and userId", 23 | "Response message bar with status code and called URL" 24 | ], 25 | "changed": [], 26 | "removed": [], 27 | "fixed": [] 28 | } 29 | }, 30 | { 31 | "version": "0.0.3", 32 | "changes": { 33 | "added": [ 34 | "Request header support", 35 | "Code snippet for SPFx solution with the performed API call", 36 | "Search enabled in code editor", 37 | "Code/line wrapping" 38 | ], 39 | "changed": [], 40 | "removed": [], 41 | "fixed": [] 42 | } 43 | }, 44 | { 45 | "version": "0.0.2", 46 | "changes": { 47 | "added": [ 48 | "Response status code is shown with each request", 49 | "Automatic conversion from JSON to TS interface" 50 | ], 51 | "changed": [], 52 | "removed": [], 53 | "fixed": [] 54 | } 55 | }, 56 | { 57 | "version": "0.0.1", 58 | "changes": { 59 | "added": ["Initial BETA version"], 60 | "changed": [], 61 | "removed": [], 62 | "fixed": [] 63 | } 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /config/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json", 3 | // Display errors as warnings 4 | "displayAsWarning": true, 5 | // The TSLint task may have been configured with several custom lint rules 6 | // before this config file is read (for example lint rules from the tslint-microsoft-contrib 7 | // project). If true, this flag will deactivate any of these rules. 8 | "removeExistingRules": true, 9 | // When true, the TSLint task is configured with some default TSLint "rules.": 10 | "useDefaultConfigAsBase": false, 11 | // Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules 12 | // which are active, other than the list of rules below. 13 | "lintConfig": { 14 | // Opt-in to Lint rules which help to eliminate bugs in JavaScript 15 | "rules": { 16 | "class-name": false, 17 | "export-name": false, 18 | "forin": false, 19 | "label-position": false, 20 | "member-access": true, 21 | "no-arg": false, 22 | "no-console": false, 23 | "no-construct": false, 24 | "no-duplicate-case": true, 25 | "no-duplicate-variable": true, 26 | "no-eval": false, 27 | "no-function-expression": true, 28 | "no-internal-module": true, 29 | "no-shadowed-variable": true, 30 | "no-switch-case-fall-through": true, 31 | "no-unnecessary-semicolons": true, 32 | "no-unused-expression": true, 33 | "no-use-before-declare": true, 34 | "no-with-statement": true, 35 | "semicolon": true, 36 | "trailing-comma": false, 37 | "typedef": false, 38 | "typedef-whitespace": false, 39 | "use-named-parameter": true, 40 | "valid-typeof": true, 41 | "variable-name": false, 42 | "whitespace": false, 43 | "no-debugger": true 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/webparts/restTester/RestTesterWebPart.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDom from 'react-dom'; 3 | import { Version } from '@microsoft/sp-core-library'; 4 | import { 5 | BaseClientSideWebPart, 6 | IPropertyPaneConfiguration, 7 | PropertyPaneLabel, 8 | PropertyPaneLink 9 | } from '@microsoft/sp-webpart-base'; 10 | 11 | // import * as strings from 'RestTesterWebPartStrings'; 12 | import RestTester from './components/RestTester'; 13 | import { IRestTesterProps } from './components/IRestTesterProps'; 14 | 15 | export interface IRestTesterWebPartProps { 16 | data: any; 17 | } 18 | 19 | export default class RestTesterWebPart extends BaseClientSideWebPart { 20 | public render(): void { 21 | const element: React.ReactElement = React.createElement( 22 | RestTester, 23 | { 24 | context: this.context 25 | } 26 | ); 27 | 28 | ReactDom.render(element, this.domElement); 29 | } 30 | 31 | protected get dataVersion(): Version { 32 | return Version.parse('1.0'); 33 | } 34 | 35 | protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { 36 | return { 37 | pages: [ 38 | { 39 | header: { 40 | description: "" 41 | }, 42 | groups: [ 43 | { 44 | groupName: "Created by Elio Struyf", 45 | groupFields: [ 46 | PropertyPaneLabel('', { 47 | text: "Thank you for using the SP Rest API Tester. I initially created this project in order to make testing the SharePoint APIs easier without the hassle of using other tools / figuring out which headers to set." 48 | }), 49 | PropertyPaneLabel('', { 50 | text: "If you have any feedback or issues, please add them to the issue list of the repository:" 51 | }), 52 | PropertyPaneLink('', { 53 | href: "https://github.com/estruyf/spfx-rest-api-tester/issues", 54 | text: "https://github.com/estruyf/spfx-rest-api-tester/issues", 55 | target: "_blank" 56 | }), 57 | PropertyPaneLabel('', { 58 | text: "If you want to know more about SharePoint / Office 365 development. Feel free to check out my blog:" 59 | }), 60 | PropertyPaneLink('', { 61 | href: "https://www.eliostruyf.com", 62 | text: "https://www.eliostruyf.com", 63 | target: "_blank" 64 | }) 65 | ] 66 | } 67 | ] 68 | } 69 | ] 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SharePoint Framework - Rest API Tester 2 | 3 | A web part that can be used for testing out SharePoint APIs without the hassle of using another application and configuring the right headers. 4 | 5 | ![Rest API Tester](./assets/api-tester-demo.gif) 6 | 7 | ## Features 8 | 9 | The web part has the following built-in features: 10 | 11 | - Stores the last used query in local browser storage. 12 | - Ability to store your favorite queries (currently stored in local storage as well). 13 | - Ability to use tokens in the API URL. These are coming from the `this.context.pageContext` object from SPFx. 14 | - API URL Suggestions 15 | - Allows you to set custom headers and body. The body disabled for GET and HEAD requests. 16 | - For every request you get the JSON response. 17 | - Automated conversion of the JSON response to a TypeScript interface. 18 | - SPFx code snippet for the performed query. 19 | 20 | ## Ready to use this web part? 21 | 22 | To make it easier, I have created an automated release process to this repository so that you do not have to clone the repository / bundle / package the solution. The latest version will always be available here: [all releases](https://github.com/estruyf/spfx-rest-api-tester/releases). 23 | 24 | > **Info**: automated release process is achieved with VSTS and an Azure Function. 25 | 26 | To install it to your tenant, you have to follow the next steps: 27 | - Download the `estruyf-rest-api-tester.sppkg` package from a release 28 | - Upload the solution package to your tenant/site-collection app catalog 29 | - Trust the solution to be available in your environment 30 | 31 | ![Trust the solution](./assets/trust-solution.png) 32 | 33 | - Create a page, and add the web part on it 34 | 35 | ![Add web part to the page](./assets/add-to-page.png) 36 | 37 | ## Changelog 38 | 39 | For the latest changes, please check out the [changelog](./CHANGELOG.md). 40 | 41 | > **Info**: changelog is driven by the [changelog.json](./changelog.json) file. This file will also be used during the automated release process to insert the right information for the release. 42 | 43 | ## Want to contribute? 44 | 45 | Contributions are more than welcome! Please target your PRs to the `DEV` branch. 46 | 47 | A great way to contribute is to enrich the known APIs file. This file is used for the API URL suggestions. The structure of this file looks like this: 48 | 49 | ```JSON 50 | { 51 | "api": [ 52 | { "method": "", "url": "_api/..." } 53 | ] 54 | } 55 | ``` 56 | 57 | The file is located here: [./src/webparts/restTester/components/known-apis.json](./src/webparts/restTester/components/known-apis.json). 58 | 59 | ## Minimal path to awesome / running your own development version 60 | 61 | So you want to run your own version of the web part. Great! Here is what you have to do: 62 | 63 | - Clone this repository 64 | - Install the project dependencies: `npm i` 65 | - Start running the local version: `gulp serve` 66 | - Start testing out the web part 67 | 68 | ## Ideas / feedback / issues 69 | 70 | Got ideas, feedback, or discovered a bug / issue? Please add these via an issue to the issue list of this repository: [issue list](https://github.com/estruyf/spfx-rest-api-tester/issues). 71 | -------------------------------------------------------------------------------- /src/webparts/restTester/components/ApiSuggestions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './RestTester.module.scss'; 3 | import { escape } from '@microsoft/sp-lodash-subset'; 4 | 5 | const knownAPIs: IKnownAPIs = require('./known-apis.json'); 6 | 7 | export interface IKnownAPIs { 8 | api: IApi[]; 9 | } 10 | 11 | export interface IApi { 12 | method: string; 13 | url: string; 14 | } 15 | 16 | export interface IApiSuggestionsProps { 17 | inputVal: string; 18 | method: string; 19 | 20 | fChangeApiUrl: (apiUrl: string) => void; 21 | } 22 | 23 | export interface IApiSuggestionsState { 24 | apiUrls: IApi[]; 25 | apiBegin: string; 26 | apiEnd: string; 27 | } 28 | 29 | export default class ApiSuggestions extends React.Component { 30 | constructor(props: IApiSuggestionsProps) { 31 | super(props); 32 | 33 | this.state = { 34 | apiUrls: [], 35 | apiBegin: "", 36 | apiEnd: "" 37 | }; 38 | } 39 | 40 | public componentDidMount(): void { 41 | this._filterApiUrls(this.props.inputVal); 42 | } 43 | 44 | public componentDidUpdate(prevProps: IApiSuggestionsProps, prevState: IApiSuggestionsState): void { 45 | if (prevProps.inputVal !== this.props.inputVal) { 46 | this._filterApiUrls(this.props.inputVal); 47 | } 48 | } 49 | 50 | private _filterApiUrls = (crntUrl: string) => { 51 | let apiBegin: string = ""; 52 | let apiEnd: string = ""; 53 | 54 | // Retrieve the required URL parts to start filtering 55 | if (crntUrl.indexOf("_api/") !== -1) { 56 | let apiSplit: string[] = crntUrl.split("_api/"); 57 | apiBegin = apiSplit[0]; 58 | apiEnd = `_api/${apiSplit[1]}`; 59 | } else if (crntUrl.indexOf("_vti_bin") !== -1) { 60 | let apiSplit: string[] = crntUrl.split("_vti_bin/"); 61 | apiBegin = apiSplit[0]; 62 | apiEnd = `_vti_bin/${apiSplit[1]}`; 63 | } 64 | 65 | // Filter the known APIs 66 | const apiUrls = knownAPIs.api.filter(u => 67 | u.method === this.props.method && u.url.toLowerCase().indexOf(apiEnd.toLowerCase()) !== -1 && u.url.toLowerCase() !== apiEnd.toLowerCase() 68 | ).sort(this._sortByUrl); 69 | 70 | this.setState({ 71 | apiUrls, 72 | apiBegin, 73 | apiEnd 74 | }); 75 | } 76 | 77 | /** 78 | * Sort array by their URL 79 | * @param a First item 80 | * @param b Second item 81 | */ 82 | private _sortByUrl(a: IApi, b: IApi): number { 83 | if(a.url.toLowerCase() < b.url.toLowerCase()) return -1; 84 | if(a.url.toLowerCase() > b.url.toLowerCase()) return 1; 85 | return 0; 86 | } 87 | 88 | private _useApiUrl = (url: string) => { 89 | this.props.fChangeApiUrl(url); 90 | } 91 | 92 | public render(): React.ReactElement { 93 | if (this.props.inputVal && this.state.apiUrls.length > 0 && this.state.apiEnd) { 94 | return ( 95 | 104 | ); 105 | } 106 | 107 | return null; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/webparts/restTester/components/HeadersInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './RestTester.module.scss'; 3 | // import styles from './HeadersInput.module.scss'; 4 | import { TextField } from 'office-ui-fabric-react/lib/components/TextField'; 5 | import { DefaultButton } from 'office-ui-fabric-react/lib/components/Button'; 6 | import { Icon } from 'office-ui-fabric-react/lib/components/Icon'; 7 | 8 | export interface IHeadersInputProps { 9 | hIndex: number; 10 | hKey: string; 11 | hValue: string; 12 | fUpdate: (i: number, key: string, value: string) => void; 13 | } 14 | 15 | export interface IHeadersInputState { 16 | hKey: string; 17 | hValue: string; 18 | } 19 | 20 | export default class HeadersInput extends React.Component { 21 | constructor(props: IHeadersInputProps) { 22 | super(props); 23 | 24 | this.state = { 25 | hKey: "", 26 | hValue: "" 27 | }; 28 | } 29 | 30 | /** 31 | * componentDidMount lifecycle hook 32 | */ 33 | public componentDidMount(): void { 34 | this.setState({ 35 | hKey: this.props.hKey, 36 | hValue: this.props.hValue 37 | }); 38 | } 39 | 40 | /** 41 | * componentWillReceiveProps lifecycle hook 42 | */ 43 | public componentWillReceiveProps(nextProps: IHeadersInputProps): void { 44 | if (nextProps.hIndex !== this.props.hIndex || 45 | nextProps.hKey !== this.props.hKey || 46 | nextProps.hValue !== this.props.hValue) { 47 | this.setState({ 48 | hKey: nextProps.hKey, 49 | hValue: nextProps.hValue 50 | }); 51 | } 52 | } 53 | 54 | /** 55 | * Update the header key 56 | */ 57 | private _updateHeaderKey = (val: string) => { 58 | this.setState({ 59 | hKey: val 60 | }); 61 | 62 | // Check if the parent needs to be updated 63 | if (val && this.state.hValue) { 64 | this.props.fUpdate(this.props.hIndex, val, this.state.hValue); 65 | } 66 | } 67 | 68 | /** 69 | * Update the header key 70 | */ 71 | private _updateHeaderVal = (val: string) => { 72 | this.setState({ 73 | hValue: val 74 | }); 75 | 76 | // Check if the parent needs to be updated 77 | if (val && this.state.hValue) { 78 | this.props.fUpdate(this.props.hIndex, this.state.hKey, val); 79 | } 80 | } 81 | 82 | /** 83 | * Clear the current header 84 | */ 85 | private _clearHeader = () => { 86 | this.setState({ 87 | hKey: "", 88 | hValue: "" 89 | }); 90 | 91 | this.props.fUpdate(this.props.hIndex, "", ""); 92 | } 93 | 94 | public render(): React.ReactElement { 95 | return ( 96 |
97 |
98 | 101 |
102 |
103 | 106 |
107 |
108 | { 109 | (this.state.hKey || this.state.hValue) && ( 110 | 111 | Clear header 112 | 113 | ) 114 | } 115 |
116 |
117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/webparts/restTester/components/RestTester.module.scss: -------------------------------------------------------------------------------- 1 | @import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss'; 2 | 3 | .restTester { 4 | > p:first-child { 5 | margin-top: 0; 6 | padding-top: 0; 7 | } 8 | 9 | .row { 10 | @include ms-Grid-row; 11 | margin-bottom: 15px; 12 | 13 | .col1 { 14 | @include ms-Grid-col; 15 | @include ms-sm1; 16 | } 17 | 18 | .col2 { 19 | @include ms-Grid-col; 20 | @include ms-sm2; 21 | } 22 | 23 | .col5 { 24 | @include ms-Grid-col; 25 | @include ms-sm5; 26 | } 27 | 28 | .col6 { 29 | @include ms-Grid-col; 30 | @include ms-sm6; 31 | } 32 | 33 | .col10 { 34 | @include ms-Grid-col; 35 | @include ms-sm10; 36 | } 37 | 38 | .col11 { 39 | @include ms-Grid-col; 40 | @include ms-sm11; 41 | } 42 | 43 | .col12 { 44 | @include ms-Grid-col; 45 | @include ms-sm12; 46 | } 47 | } 48 | 49 | button { 50 | margin-right: 15px; 51 | } 52 | 53 | .messageBar { 54 | margin-bottom: 15px; 55 | } 56 | 57 | .respMessageBar { 58 | margin-bottom: 5px; 59 | } 60 | 61 | .title { 62 | @include ms-font-xl; 63 | color: $ms-color-themePrimary; 64 | display: block; 65 | position: relative; 66 | 67 | .credits { 68 | @include ms-font-m; 69 | bottom: 0; 70 | position: absolute; 71 | right: 0; 72 | } 73 | 74 | a { 75 | color: $ms-color-themePrimary; 76 | font-style: italic; 77 | text-decoration: none; 78 | 79 | &:hover, &:visited { 80 | color: $ms-color-themePrimary; 81 | text-decoration: underline; 82 | } 83 | } 84 | } 85 | 86 | .storedTitle, .queryTitle { 87 | @include ms-font-l; 88 | } 89 | 90 | .deleteQuery { 91 | button { 92 | width: 100%; 93 | } 94 | } 95 | 96 | .description { 97 | @include ms-fontSize-m; 98 | } 99 | 100 | .methodSelector { 101 | :global { 102 | .ms-Dropdown { 103 | &:focus { 104 | color: $ms-color-white; 105 | } 106 | } 107 | 108 | .ms-Dropdown-title { 109 | background-color: $ms-color-themePrimary; 110 | color: $ms-color-white; 111 | } 112 | 113 | .ms-Dropdown-caretDownWrapper i { 114 | color: $ms-color-white; 115 | } 116 | } 117 | } 118 | 119 | .spinner { 120 | display: inline-block; 121 | margin-left: 15px; 122 | margin-top: 6px; 123 | } 124 | 125 | .icon { 126 | margin-right: 5px; 127 | } 128 | 129 | .resultSection { 130 | .title { 131 | margin-bottom: 5px; 132 | } 133 | } 134 | 135 | .tabs { 136 | margin-bottom: 15px; 137 | position: relative; 138 | } 139 | 140 | .selectedTab { 141 | border-bottom: 1px solid; 142 | border-bottom-color: $ms-color-themePrimary; 143 | } 144 | 145 | button.codeWrap { 146 | margin-right: 0; 147 | position: absolute; 148 | right: 0; 149 | top: 0; 150 | } 151 | 152 | .codeZone { 153 | border: 1px solid; 154 | border-color: $ms-color-neutralLight; 155 | margin-bottom: 15px; 156 | overflow-x: auto; 157 | } 158 | } 159 | 160 | .queryInput { 161 | position: relative; 162 | } 163 | 164 | .suggestions { 165 | background-color: $ms-color-white; 166 | border: 1px solid; 167 | border-color: $ms-color-neutralTertiaryAlt; 168 | max-height: 200px; 169 | left: 8px; 170 | list-style: none; 171 | overflow-y: auto; 172 | padding: 0; 173 | position: absolute; 174 | right: 8px; 175 | z-index: 99; 176 | 177 | a { 178 | color: $ms-color-neutralPrimary; 179 | display: block; 180 | padding: 10px 15px; 181 | text-decoration: none; 182 | 183 | &:hover { 184 | background-color: $ms-color-neutralLighterAlt; 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/webparts/restTester/components/SnippetBuilder.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './RestTester.module.scss'; 3 | // import styles from './SnippetBuilder.module.scss'; 4 | import * as brace from 'brace'; 5 | import AceEditor from 'react-ace'; 6 | import 'brace/mode/typescript'; 7 | import 'brace/theme/github'; 8 | import 'brace/ext/searchbox'; 9 | import { IRequestInfo } from './RestTester'; 10 | import { isEmpty } from '@microsoft/sp-lodash-subset'; 11 | import * as beautify from 'js-beautify'; 12 | 13 | export interface ISnippetBuilderProps { 14 | requestInfo: IRequestInfo; 15 | wrapCode: boolean; 16 | } 17 | 18 | export interface ISnippetBuilderState { 19 | code: string; 20 | } 21 | 22 | const codeSnippet = `const apiUrl = \`{apiUrl}\`; 23 | this.context.spHttpClient.fetch(apiUrl, SPHttpClient.configurations.v1, { 24 | method: {requestType}{headers}{body} 25 | }) 26 | .then((data: SPHttpClientResponse) => data.json()) 27 | .then((data: any) => { 28 | // Write your code here 29 | });`; 30 | 31 | export default class SnippetBuilder extends React.Component { 32 | constructor(props: ISnippetBuilderProps) { 33 | super(props); 34 | 35 | this.state = { 36 | code: codeSnippet 37 | }; 38 | } 39 | 40 | public componentDidMount(): void { 41 | this._updateCodeSnippet(this.props); 42 | } 43 | 44 | public componentWillReceiveProps(nextProps: ISnippetBuilderProps): void { 45 | this._updateCodeSnippet(nextProps); 46 | } 47 | 48 | /** 49 | * Update the tokens in the body and URL 50 | */ 51 | private _updateTokens = (val: string) => { 52 | val = val.replace(/{webUrl}/g, "${this.context.pageContext.web.absoluteUrl}"); 53 | val = val.replace(/{webId}/g, "${this.context.pageContext.web.id}"); 54 | val = val.replace(/{listId}/g, "${this.context.pageContext.list.id}"); 55 | val = val.replace(/{itemId}/g, "${this.context.pageContext.listItem.id}"); 56 | val = val.replace(/{siteId}/g, "${this.context.pageContext.site.id}"); 57 | val = val.replace(/{userId}/g, "${this.context.pageContext.legacyPageContext.userId}"); 58 | return val; 59 | } 60 | 61 | private _updateCodeSnippet = (props: ISnippetBuilderProps) => { 62 | if (!props.requestInfo) { 63 | this.setState({ 64 | code: "" 65 | }); 66 | return; 67 | } 68 | 69 | let snippet = codeSnippet; 70 | 71 | // Update the API URL 72 | let apiUrl = props.requestInfo.url; 73 | apiUrl = this._updateTokens(apiUrl); 74 | snippet = snippet.replace("{apiUrl}", apiUrl); 75 | 76 | // Update the request type 77 | snippet = snippet.replace("{requestType}", `"${props.requestInfo.method}"`); 78 | 79 | // Update the headers if there were any provided 80 | if (isEmpty(props.requestInfo.headers)) { 81 | snippet = snippet.replace("{headers}", ""); 82 | } else { 83 | snippet = snippet.replace("{headers}", `, 84 | headers: ${JSON.stringify(props.requestInfo.headers, null, 4)}`); 85 | } 86 | 87 | // Update the body if it is a post request 88 | if (props.requestInfo.method === "POST" && props.requestInfo.body) { 89 | snippet = snippet.replace("{body}", `, 90 | body: JSON.stringify({body})`); 91 | let body = props.requestInfo.body; 92 | body = this._updateTokens(body); 93 | snippet = snippet.replace("{body}", body); 94 | } else { 95 | snippet = snippet.replace("{body}", ""); 96 | } 97 | 98 | // Used "as any" because unindent_chained_methods is a new setting and not yet in the typings 99 | this.setState({ 100 | code: beautify(snippet, { indent_size: 2, unindent_chained_methods: true } as any) 101 | }); 102 | } 103 | 104 | public render(): React.ReactElement { 105 | return ( 106 | 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/webparts/restTester/components/ResponseInfo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './RestTester.module.scss'; 3 | import { ActionButton } from 'office-ui-fabric-react/lib/components/Button'; 4 | import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; 5 | import AceEditor from 'react-ace'; 6 | import SnippetBuilder from './SnippetBuilder'; 7 | import { ResultType, IRequestInfo } from './RestTester'; 8 | import jsonToTS from 'json-to-ts'; 9 | import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/components/MessageBar'; 10 | 11 | export interface IResponseInfoProps { 12 | status: number | string; 13 | resultType: ResultType; 14 | wrapCode: boolean; 15 | requestInfo: IRequestInfo; 16 | data: string; 17 | 18 | fSwitchTab: (val: ResultType) => void; 19 | fTriggerCodeWrap: (ev: React.FormEvent, isChecked: boolean) => void; 20 | } 21 | 22 | export interface IResponseInfoState {} 23 | 24 | export default class ResponseInfo extends React.Component { 25 | public render(): React.ReactElement { 26 | // Stringify the rest response 27 | const restResponse: string = this.props.data ? JSON.stringify(this.props.data, null, 2) : ""; 28 | // Create the TS interface 29 | const interfaceObj: string = this.props.data ? jsonToTS(this.props.data).join("\n\n") : ""; 30 | 31 | return ( 32 |
33 |

API Result

34 | 35 | { 36 | this.props.status && ( 37 | = 200 && this.props.status < 300) ? MessageBarType.success : MessageBarType.error}> 38 | Status code: {this.props.status} {(this.props.requestInfo && this.props.requestInfo.absUrl) && - Called URL: {this.props.requestInfo.absUrl}} 39 | 40 | ) 41 | } 42 | 43 |
44 | this.props.fSwitchTab(ResultType.body)} className={`${this.props.resultType === ResultType.body && styles.selectedTab}`}> 45 | Response preview 46 | 47 | 48 | this.props.fSwitchTab(ResultType.interface)} className={`${this.props.resultType === ResultType.interface && styles.selectedTab}`}> 49 | TypeScript interface 50 | 51 | 52 | this.props.fSwitchTab(ResultType.codeSnippet)} className={`${this.props.resultType === ResultType.codeSnippet && styles.selectedTab}`}> 53 | SPFx code snippet 54 | 55 | 56 | 60 |
61 | 62 | { 63 | this.props.resultType === ResultType.body && ( 64 | 76 | ) 77 | } 78 | { 79 | this.props.resultType === ResultType.interface && ( 80 | 92 | ) 93 | } 94 | { 95 | this.props.resultType === ResultType.codeSnippet && ( 96 | 97 | ) 98 | } 99 |
100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/webparts/restTester/components/RestTester.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './RestTester.module.scss'; 3 | import { IRestTesterProps } from './IRestTesterProps'; 4 | import { escape } from '@microsoft/sp-lodash-subset'; 5 | import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http'; 6 | import { TextField } from 'office-ui-fabric-react/lib/TextField'; 7 | import { DefaultButton, ActionButton } from 'office-ui-fabric-react/lib/Button'; 8 | import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; 9 | import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; 10 | import { Label } from 'office-ui-fabric-react/lib/Label'; 11 | import { Icon } from 'office-ui-fabric-react/lib/Icon'; 12 | import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; 13 | import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar'; 14 | import * as brace from 'brace'; 15 | import AceEditor from 'react-ace'; 16 | import 'brace/mode/json'; 17 | import 'brace/mode/typescript'; 18 | import 'brace/theme/github'; 19 | import 'brace/ext/searchbox'; 20 | import HeadersInput from './HeadersInput'; 21 | import SnippetBuilder from './SnippetBuilder'; 22 | import ResponseInfo from './ResponseInfo'; 23 | import ApiSuggestions from './ApiSuggestions'; 24 | 25 | /** 26 | * TODO: Allow other API support (not MS Graph) 27 | * - Check for SP URL or MS Graph (show URL to Graph Explorer) 28 | */ 29 | 30 | export enum ResultType { 31 | body = 1, 32 | interface, 33 | codeSnippet 34 | } 35 | 36 | export enum RequestTab { 37 | body = 1, 38 | headers 39 | } 40 | 41 | export enum Methods { 42 | GET = 1, 43 | POST, 44 | PUT, 45 | PATCH, 46 | DELETE, 47 | HEAD 48 | } 49 | 50 | export interface IStoredQuery { 51 | requestType: Methods | string; 52 | apiUrl: string; 53 | reqBody: string; 54 | customHeaders: IHeader[]; 55 | } 56 | 57 | export interface IRestTesterState extends IStoredQuery { 58 | data: any; 59 | status: number | string; 60 | loading: boolean; 61 | cached: boolean; 62 | storage: boolean; 63 | storedQueries: IDropdownOption[]; 64 | selectedStoredQuery: number | string; 65 | resultType: ResultType; 66 | wrapCode: boolean; 67 | requestTab: RequestTab; 68 | requestInfo: IRequestInfo; 69 | showSuggestions: boolean; 70 | } 71 | 72 | export interface IHeader { 73 | key: string; 74 | value: string; 75 | } 76 | 77 | export interface IRequestInfo { 78 | url: string; 79 | absUrl: string; 80 | method: string; 81 | headers: HeadersInit; 82 | body: string; 83 | } 84 | 85 | export default class RestTester extends React.Component { 86 | private _allQueries: IStoredQuery[] = []; 87 | 88 | constructor(props: IRestTesterProps) { 89 | super(props); 90 | 91 | // Set the all query empty array 92 | this._allQueries = []; 93 | 94 | // Initialize state 95 | this.state = { 96 | requestType: Methods.GET, 97 | apiUrl: `${this.props.context.pageContext.web.absoluteUrl}/_api/web`, 98 | reqBody: "{}", 99 | data: "", 100 | status: null, 101 | loading: false, 102 | cached: false, 103 | storage: typeof localStorage !== "undefined", 104 | storedQueries: [], 105 | selectedStoredQuery: null, 106 | resultType: ResultType.body, 107 | wrapCode: false, 108 | customHeaders: [{ key: "", value: "" }], 109 | requestTab: RequestTab.body, 110 | requestInfo: null, 111 | showSuggestions: false 112 | }; 113 | } 114 | 115 | /** 116 | * Default React componentDidMount method 117 | */ 118 | public componentDidMount(): void { 119 | // Fetch previous query from local storage 120 | this._fetchFromStorage(); 121 | } 122 | 123 | /** 124 | * Default React componentDidUpdate method 125 | * @param prevProps 126 | * @param prevState 127 | */ 128 | public componentDidUpdate(prevProps: IRestTesterProps, prevState: IRestTesterState): void { 129 | if (this.state.cached) { 130 | this._runQuery(); 131 | } 132 | } 133 | 134 | /** 135 | * Event handler for request mode change 136 | */ 137 | private _requestChanged = (val: IDropdownOption) => { 138 | this.setState({ 139 | requestType: val.key as number, 140 | reqBody: "{}" 141 | }); 142 | } 143 | 144 | /** 145 | * Event handler for api URL change 146 | */ 147 | private _apiUrlChanged = (val: string) => { 148 | this.setState({ 149 | apiUrl: val 150 | }); 151 | } 152 | 153 | /** 154 | * Request body value changed 155 | */ 156 | private _reqBodyChanged = (val: string) => { 157 | this.setState({ 158 | reqBody: val 159 | }); 160 | } 161 | 162 | /** 163 | * Store the latest query in local storage 164 | */ 165 | private _storeLastQuery = () => { 166 | if (this.state.storage) { 167 | const toStore: IStoredQuery = { 168 | requestType: Methods[this.state.requestType], 169 | apiUrl: this.state.apiUrl, 170 | reqBody: this.state.reqBody, 171 | customHeaders: this.state.customHeaders 172 | }; 173 | 174 | localStorage.setItem(`resttester-apiUrl-${this.props.context.manifest.id}`, JSON.stringify(toStore)); 175 | } 176 | } 177 | 178 | /** 179 | * Fetch the query from the browser storage 180 | */ 181 | private _fetchFromStorage = () => { 182 | if (this.state.storage) { 183 | // Fetch the last stored query 184 | const storedQuery: string = localStorage.getItem(`resttester-apiUrl-${this.props.context.manifest.id}`); 185 | if (storedQuery) { 186 | const parsedQuery: IStoredQuery = JSON.parse(storedQuery); 187 | 188 | this.setState({ 189 | requestType: typeof parsedQuery.requestType === "string" ? Methods[parsedQuery.requestType] : parsedQuery.requestType, 190 | apiUrl: parsedQuery.apiUrl, 191 | reqBody: parsedQuery.reqBody, 192 | customHeaders: parsedQuery.customHeaders ? parsedQuery.customHeaders : [{ key: "", value: "" }], 193 | cached: true 194 | }); 195 | } 196 | 197 | // Fetch all the stored queries 198 | const storedQueries: string = localStorage.getItem(`resttester-allqueries-${this.props.context.manifest.id}`); 199 | if (storedQueries) { 200 | this._allQueries = JSON.parse(storedQueries); 201 | this._updateQueriesDropdown(); 202 | } 203 | } else { 204 | // Run the query because browser doesn't support local storage 205 | this._runQuery(); 206 | } 207 | } 208 | 209 | /** 210 | * Store the current query 211 | */ 212 | private _saveCurrentQuery = () => { 213 | if (this.state.storage) { 214 | // Get all stored queries 215 | const storedQueries: string = localStorage.getItem(`resttester-allqueries-${this.props.context.manifest.id}`); 216 | if (!storedQueries) { 217 | this._allQueries = []; 218 | } else { 219 | this._allQueries = JSON.parse(storedQueries); 220 | } 221 | 222 | // Add the current query to the list 223 | this._allQueries.push({ 224 | requestType: Methods[this.state.requestType], 225 | apiUrl: this.state.apiUrl, 226 | reqBody: this.state.reqBody, 227 | customHeaders: this.state.customHeaders 228 | }); 229 | 230 | // Update the stored queries dropdown with the new values 231 | this._updateQueriesDropdown(); 232 | 233 | // Update local storage 234 | localStorage.setItem(`resttester-allqueries-${this.props.context.manifest.id}`, JSON.stringify(this._allQueries)); 235 | } 236 | } 237 | 238 | /** 239 | * Update the current selected query 240 | */ 241 | private _useSelectedQuery = (val: IDropdownOption) => { 242 | // Check if one of the known values got selected 243 | if (typeof val.key === "number" && this._allQueries) { 244 | const newQuery = this._allQueries[val.key]; 245 | this.setState({ 246 | selectedStoredQuery: val.key, 247 | requestType: typeof newQuery.requestType === "string" ? Methods[newQuery.requestType] : newQuery.requestType, 248 | apiUrl: newQuery.apiUrl, 249 | reqBody: newQuery.reqBody, 250 | customHeaders: newQuery.customHeaders ? newQuery.customHeaders : [{ key: "", value: "" }] 251 | }); 252 | } else { 253 | this.setState({ 254 | selectedStoredQuery: val.key as number 255 | }); 256 | } 257 | } 258 | 259 | /** 260 | * Delete the currently selected query 261 | */ 262 | private _deleteCrntQuery = () => { 263 | if (typeof this.state.selectedStoredQuery === "number") { 264 | // Remove the stored query 265 | this._allQueries.splice(this.state.selectedStoredQuery, 1); 266 | // Update the values in the storage 267 | localStorage.setItem(`resttester-allqueries-${this.props.context.manifest.id}`, JSON.stringify(this._allQueries)); 268 | // Get the new available queries 269 | this._updateQueriesDropdown(); 270 | // Update the component state 271 | this.setState({ 272 | selectedStoredQuery: null 273 | }); 274 | } 275 | } 276 | 277 | /** 278 | * Update the elements in the stored queries dropdown 279 | */ 280 | private _updateQueriesDropdown = () => { 281 | let ddOpts: IDropdownOption[] = this._allQueries.map((q: IStoredQuery, index: number) => ({ 282 | key: index, 283 | text: `${q.requestType}: ${q.apiUrl}` 284 | })); 285 | 286 | this.setState({ 287 | storedQueries: ddOpts 288 | }); 289 | } 290 | 291 | /** 292 | * Update the tokens in the body and URL 293 | */ 294 | private _updateTokens = (val: string) => { 295 | val = val.replace(/{webUrl}/g, this.props.context.pageContext.web.absoluteUrl); 296 | val = val.replace(/{webId}/g, this.props.context.pageContext.web.id.toString()); 297 | val = val.replace(/{listId}/g, this.props.context.pageContext.list.id.toString()); 298 | val = val.replace(/{itemId}/g, this.props.context.pageContext.listItem.id.toString()); 299 | val = val.replace(/{siteId}/g, this.props.context.pageContext.site.id.toString()); 300 | val = val.replace(/{userId}/g, this.props.context.pageContext.legacyPageContext.userId); 301 | return val; 302 | } 303 | 304 | /** 305 | * Runs the specified query against SharePoint 306 | */ 307 | private _runQuery = () => { 308 | this.setState({ 309 | loading: true, 310 | data: "", 311 | status: null, 312 | cached: false 313 | }); 314 | 315 | // Hiding the suggestions 316 | this._hideSuggestions(); 317 | 318 | // Get state properties 319 | let { apiUrl, requestType, reqBody, customHeaders } = this.state; 320 | 321 | // Store the performed query 322 | this._storeLastQuery(); 323 | 324 | // Add the current request method 325 | let reqOptions: ISPHttpClientOptions = { 326 | method: Methods[requestType] 327 | }; 328 | 329 | // Check if a body needs to be added to the request 330 | if (requestType !== Methods.GET && requestType !== Methods.HEAD && reqBody) { 331 | reqBody = this._updateTokens(reqBody); 332 | reqOptions["body"] = reqBody; 333 | } 334 | 335 | // Create new headers object 336 | const reqHeaders: HeadersInit = {}; 337 | 338 | // Check the search API is used 339 | if (apiUrl.toLowerCase().indexOf('_api/search') !== -1) { 340 | reqHeaders["odata-version"] = "3.0"; 341 | } 342 | 343 | // Set all custom headers 344 | if (customHeaders.length > 1) { 345 | // Add all custom set headers 346 | for (const header of customHeaders) { 347 | if (header.key) { 348 | reqHeaders[header.key] = header.value; 349 | } 350 | } 351 | } 352 | 353 | // Add all headers to the options object 354 | reqOptions["headers"] = reqHeaders; 355 | 356 | // Update tokens in the URL 357 | apiUrl = this._updateTokens(apiUrl); 358 | 359 | try { 360 | this.props.context.spHttpClient.fetch(apiUrl, SPHttpClient.configurations.v1, reqOptions) 361 | .then((data: SPHttpClientResponse) => { 362 | this.setState({ 363 | status: data.status 364 | }); 365 | return data.json(); 366 | }) 367 | .then((data: any) => { 368 | this.setState({ 369 | data: data, 370 | loading: false, 371 | requestInfo: { 372 | url: this.state.apiUrl, 373 | absUrl: apiUrl, 374 | method: reqOptions.method, 375 | headers: reqOptions.headers, 376 | body: this.state.reqBody 377 | } 378 | }); 379 | }).catch(err => { 380 | this.setState({ 381 | data: err, 382 | loading: false, 383 | status: "Error", 384 | requestInfo: null 385 | }); 386 | }); 387 | } catch (err) { 388 | this.setState({ 389 | data: err && err.message && err.stack ? { msg: err.message, stack: err.stack } : "Something went wrong, you might find a clue in the browser console.", 390 | loading: false, 391 | status: "Error" 392 | }); 393 | } 394 | } 395 | 396 | /** 397 | * Switch the request tab 398 | */ 399 | private _switchRequestTab = (val: RequestTab): void => { 400 | this.setState({ 401 | requestTab: val 402 | }); 403 | } 404 | 405 | /** 406 | * Switch the result tab 407 | */ 408 | private _switchResultTab = (val: ResultType): void => { 409 | this.setState({ 410 | resultType: val 411 | }); 412 | } 413 | 414 | /** 415 | * Trigger code wrapping 416 | */ 417 | private _triggerCodeWrapping = (ev: React.FormEvent, isChecked: boolean): void => { 418 | this.setState({ 419 | wrapCode: isChecked 420 | }); 421 | } 422 | 423 | /** 424 | * Trigger an header update 425 | */ 426 | private _updateHeader = (i: number, key: string, value: string): void => { 427 | const allHeaders = [...this.state.customHeaders]; 428 | 429 | // Check if key and value contain data 430 | if (!key && !value) { 431 | // Remove item 432 | allHeaders.splice(i, 1); 433 | 434 | // Check if a new item needs to be added 435 | if (allHeaders.length === 0) { 436 | // Add an new empty item 437 | allHeaders.push({ key: "", value: "" }); 438 | } 439 | } else { 440 | // Update the current item 441 | allHeaders[i].key = key; 442 | allHeaders[i].value = value; 443 | 444 | // Check if the last item is still empty, otherwise we need to add a new header 445 | const lastItem = allHeaders[allHeaders.length-1]; 446 | if (lastItem.key) { 447 | // Add an new empty item 448 | allHeaders.push({ key: "", value: "" }); 449 | } 450 | } 451 | 452 | this.setState({ 453 | customHeaders: allHeaders 454 | }); 455 | } 456 | 457 | /** 458 | * Update the API URL from the suggestion 459 | */ 460 | private _updateApiUrl = (apiUrl: string) => { 461 | this.setState({ 462 | apiUrl 463 | }); 464 | // Hiding the suggestions 465 | this._hideSuggestions(); 466 | } 467 | 468 | /** 469 | * Trigger the suggestions to show 470 | */ 471 | private _showSuggestions = () => { 472 | this.setState({ 473 | showSuggestions: true 474 | }); 475 | } 476 | 477 | /** 478 | * Trigger the suggestions to hide 479 | */ 480 | private _hideSuggestions = () => { 481 | this.setState({ 482 | showSuggestions: false 483 | }); 484 | } 485 | 486 | /** 487 | * Default React render mothod 488 | */ 489 | public render(): React.ReactElement { 490 | return ( 491 |
492 | API tester this.props.context.propertyPane.open()} title="Elio Struyf">Created by Elio Struyf 493 | 494 | { 495 | this.state.storage && ( 496 |
497 |
498 |

Use one of your stored API calls

499 |
500 |
501 | 508 |
509 |
510 | 511 | Delete query 512 | 513 |
514 |
515 | ) 516 | } 517 | 518 |

Modify your API call

519 | 520 |

{`The following tokens can be used in the URL and body fields: {siteId} | {webId} | {webUrl} | {listId} | {itemId} | {userId}`}

521 | 522 |
523 |
524 | 535 |
536 |
537 | ) => e.key === "Enter" && this._runQuery()} 541 | onFocus={this._showSuggestions} 542 | onBlur={() => setTimeout(() => this._hideSuggestions(), 500)} /> 543 | 544 | { 545 | this.state.showSuggestions && ( 546 | 549 | ) 550 | } 551 |
552 |
553 | 554 |
555 | this._switchRequestTab(RequestTab.body)} className={`${this.state.requestTab === RequestTab.body && styles.selectedTab}`}> 556 | Request body 557 | 558 | 559 | this._switchRequestTab(RequestTab.headers)} className={`${this.state.requestTab === RequestTab.headers && styles.selectedTab}`}> 560 | Request headers { this.state.customHeaders.length > 1 && `(${this.state.customHeaders.length - 1})` } 561 | 562 |
563 | 564 | { 565 | this.state.requestTab === RequestTab.body ? ( 566 | this.state.requestType !== Methods.GET && this.state.requestType !== Methods.HEAD ? ( 567 | 578 | ) : ( 579 | 580 | Body not supported with GET/HEAD requests 581 | 582 | ) 583 | ) : ( 584 |
585 | { 586 | this.state.customHeaders.map((ch: IHeader, index: number) => ( 587 | 588 | )) 589 | } 590 |
591 | ) 592 | } 593 | 594 | 595 | Store query 596 | 597 | 598 | 600 | Run query 601 | 602 | 603 | { 604 | this.state.loading && 605 | } 606 | 607 | { 608 | /** 609 | * Result information 610 | */ 611 | } 612 | 619 |
620 | ); 621 | } 622 | } 623 | --------------------------------------------------------------------------------