├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── .styleci.yml ├── changelog.md ├── composer.json ├── config └── laravelapiexplorer.php ├── contributing.md ├── license.md ├── package.json ├── phpunit.xml ├── readme.md ├── resources ├── assets │ ├── build │ │ └── bundle.js │ └── js │ │ ├── components │ │ ├── App │ │ │ ├── App.js │ │ │ └── index.js │ │ ├── ArgumentsList │ │ │ ├── ArgumentsList.js │ │ │ └── index.js │ │ ├── ChipHttpVerb │ │ │ ├── ChipHttpVerb.js │ │ │ └── index.js │ │ ├── DescriptionTable │ │ │ ├── DescriptionTable.js │ │ │ └── index.js │ │ ├── InfoList │ │ │ ├── InfoList.js │ │ │ └── index.js │ │ ├── ModalSetings │ │ │ ├── ModalSetings.js │ │ │ ├── SettingsPanel.js │ │ │ └── index.js │ │ ├── RoutePlayground │ │ │ ├── Drawer │ │ │ │ ├── DrawerResponse.js │ │ │ │ ├── DrawerRoute.js │ │ │ │ ├── DrawerWrapper.js │ │ │ │ └── RouteInfo.js │ │ │ ├── JsonEditor.js │ │ │ ├── JsonViewer.js │ │ │ ├── Panel.js │ │ │ ├── RequestArea.js │ │ │ ├── ResponseArea.js │ │ │ ├── RoutePlayground.js │ │ │ └── index.js │ │ ├── RoutesList │ │ │ ├── RouteListItem.js │ │ │ ├── RoutesList.js │ │ │ └── index.js │ │ └── TabPanel │ │ │ ├── TabPanel.js │ │ │ └── index.js │ │ ├── main.js │ │ └── utils │ │ ├── hash.js │ │ ├── request.js │ │ ├── sharedPropTypes.js │ │ ├── storage.js │ │ └── string.js └── views │ └── main.blade.php ├── src ├── Facades │ └── LaravelApiExplorer.php ├── LaravelApiExplorer.php ├── LaravelApiExplorerController.php ├── LaravelApiExplorerServiceProvider.php └── routes.php ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties", 5 | "@babel/plugin-proposal-object-rest-spread" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true, 5 | "es6": true 6 | }, 7 | "extends": ["eslint:recommended", "plugin:react/recommended"], 8 | "parser": "babel-eslint", 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "jsx": true 12 | }, 13 | "ecmaVersion": 2018, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["react", "babel", "react-hooks"], 17 | "rules": { 18 | "indent": ["error", 4], 19 | "linebreak-style": ["error", "unix"], 20 | "quotes": ["error", "double"], 21 | "semi": ["error", "never"] 22 | }, 23 | "settings": { 24 | "react": { 25 | "version": "detect" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": false, 4 | "singleQuote": false 5 | } 6 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `ApiExplorer` will be documented in this file. 4 | 5 | ## Version 1.0 6 | 7 | ### Added 8 | - Everything 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netojose/laravel-api-explorer", 3 | "description": "API explorer for laravel application", 4 | "version": "2.0.1", 5 | "minimum-stability": "dev", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "José Neto", 10 | "email": "sputinykster@gmail.com", 11 | "homepage": "https://netojose.github.io/" 12 | } 13 | ], 14 | "homepage": "https://github.com/netojose/laravel-api-explorer", 15 | "keywords": [ 16 | "laravel", 17 | "api", 18 | "rest", 19 | "laravel-api-explorer" 20 | ], 21 | "autoload": { 22 | "psr-4": { 23 | "NetoJose\\LaravelApiExplorer\\": "src/" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "NetoJose\\LaravelApiExplorer\\Tests\\": "tests" 29 | } 30 | }, 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "NetoJose\\LaravelApiExplorer\\LaravelApiExplorerServiceProvider" 35 | ], 36 | "aliases": { 37 | "LaravelApiExplorer": "NetoJose\\LaravelApiExplorer\\Facades\\LaravelApiExplorer" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/laravelapiexplorer.php: -------------------------------------------------------------------------------- 1 | true, 6 | 7 | 'route' => 'api-explorer', 8 | 9 | 'match' => 'api/*', 10 | 11 | 'ignore' => [ 12 | '/' 13 | ], 14 | 15 | ]; -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited. 4 | 5 | Contributions are accepted via Pull Requests on [Github](https://github.com/netojose/apiexplorer). 6 | 7 | # Things you could do 8 | 9 | If you want to contribute but do not know where to start, this list provides some starting points 10 | 11 | ## Pull Requests 12 | 13 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 14 | 15 | - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date. 16 | 17 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 18 | 19 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 20 | 21 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 22 | 23 | **Happy coding**! 24 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # The license 2 | 3 | Copyright (c) José Neto 4 | 5 | ...Add your license text here... -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apiexplorer", 3 | "version": "2.0.1", 4 | "description": "API explorer front end", 5 | "main": "index.js", 6 | "keywords": [ 7 | "API", 8 | "explorer", 9 | "React", 10 | "Laravel" 11 | ], 12 | "author": "José Neto", 13 | "license": "MIT", 14 | "scripts": { 15 | "dev": "webpack ---mode=development", 16 | "dev:watch": "webpack ---mode=development --watch", 17 | "build": "webpack ---mode=production" 18 | }, 19 | "dependencies": { 20 | "@material-ui/core": "^4.5.0", 21 | "@material-ui/icons": "^4.4.3", 22 | "axios": "^0.28.0", 23 | "hash-sum": "^2.0.0", 24 | "jsoneditor": "^9.5.6", 25 | "prop-types": "^15.7.2", 26 | "react": "^16.10.2", 27 | "react-dom": "^16.10.2" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.6.2", 31 | "@babel/plugin-proposal-class-properties": "^7.5.5", 32 | "@babel/plugin-proposal-object-rest-spread": "^7.6.2", 33 | "@babel/preset-env": "^7.6.2", 34 | "@babel/preset-react": "^7.0.0", 35 | "babel-eslint": "^10.0.3", 36 | "babel-loader": "^8.0.6", 37 | "css-loader": "^3.2.0", 38 | "eslint": "^6.5.1", 39 | "eslint-plugin-babel": "^5.3.0", 40 | "eslint-plugin-react": "^7.16.0", 41 | "eslint-plugin-react-hooks": "^2.1.2", 42 | "style-loader": "^1.0.0", 43 | "svg-url-loader": "^3.0.2", 44 | "webpack": "^4.41.0", 45 | "webpack-cli": "^3.3.9" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel API explorer 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Total Downloads][ico-downloads]][link-downloads] 5 | 6 | Interactive Laravel API explorer. You don't need to write/update documentation for your API. On the fly, Your API documentation will always be available in an interactive way. 7 | 8 | ## Features 9 | 10 | - Quick install (one-step install, no code change needed); 11 | - Zero config needed; 12 | - Store config/parameters to be used anytime; 13 | - Variables: you can set variables (like id's, tokens, etc. to be used in any place like querystring, header, body, etc.); 14 | - Global headers: You can set global headers (like tokens, content-type, etc.) to be used in all requests. 15 | 16 | ## Live Demo 17 | 18 | https://laravel-api-explorer-demo.herokuapp.com/api-explorer 19 | 20 | ## Using variables 21 | 22 | You can click on top right icon (wrench) and add your variables. When you will need to set some querystring parameter, header value, body content, etc., you can use `${VARIABLE_NAME}`, and this placeholder will be replaced by your variable. 23 | 24 | ## Using global headers 25 | 26 | If you API needs some header in all request (or almost), you can set global headers instead of create these headers for every request. You can click on top right icon (wrench) and add your global headers. 27 | 28 | ## Screenshots 29 | 30 | ### Routes list 31 | 32 | ![Routes list][screenshot-1] 33 | 34 | ### Route info 35 | 36 | ![Route info][screenshot-2] 37 | 38 | ### Request/response 39 | 40 | ![Request/response][screenshot-3] 41 | 42 | ### Response info 43 | 44 | ![Response info][screenshot-4] 45 | 46 | ## Installation 47 | 48 | Via Composer 49 | 50 | ```bash 51 | $ composer require netojose/laravel-api-explorer 52 | ``` 53 | 54 | ## Usage 55 | 56 | You just need access `yourdomain.com/api-explorer` 57 | 58 | ## Configuration 59 | 60 | Optionally you can copy config file to override default package configuration 61 | 62 | ```bash 63 | php artisan vendor:publish --provider="NetoJose\LaravelApiExplorer\LaravelApiExplorerServiceProvider" 64 | ``` 65 | 66 | Now you have a `config/laravelapiexplorer.php` file inside your project,and you can make your changes. Available configurations: 67 | 68 | | Configuration | Description | Default | 69 | | ------------- | ---------------------------------------------------------------------------------- | ------------ | 70 | | enabled | Determine if the explorer will available | true | 71 | | route | The route to access explorer page | api-explorer | 72 | | match | Pattern to routes to be available on explorer | api/\* | 73 | | ignore | Array of routes to be ignored. You can use a pattern of a route path or route name | [,'/',] | 74 | 75 | ## Contributing 76 | 77 | Please see [contributing.md](contributing.md) for details and a todolist. 78 | 79 | ## Security 80 | 81 | If you discover any security related issues, please email sputinykster@gmail.com instead of using the issue tracker. 82 | 83 | [ico-version]: https://img.shields.io/packagist/v/netojose/laravel-api-explorer.svg?style=flat-square 84 | [ico-downloads]: https://img.shields.io/packagist/dt/netojose/laravel-api-explorer.svg?style=flat-square 85 | [link-packagist]: https://packagist.org/packages/netojose/laravel-api-explorer 86 | [link-downloads]: https://packagist.org/packages/netojose/laravel-api-explorer 87 | [link-author]: https://netojose.github.io 88 | [screenshot-1]: https://i.imgur.com/MA27Djs.png "Routes list" 89 | [screenshot-2]: https://i.imgur.com/lZrCPUz.png "Route info" 90 | [screenshot-3]: https://i.imgur.com/dfXlxiV.png "Request/response" 91 | [screenshot-4]: https://i.imgur.com/ApPO9Au.png "Response info" 92 | -------------------------------------------------------------------------------- /resources/assets/js/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useMemo, useCallback } from "react" 2 | import { makeStyles } from "@material-ui/core/styles" 3 | import Grid from "@material-ui/core/Grid" 4 | import AppBar from "@material-ui/core/AppBar" 5 | import Toolbar from "@material-ui/core/Toolbar" 6 | import Typography from "@material-ui/core/Typography" 7 | import Box from "@material-ui/core/Box" 8 | import Paper from "@material-ui/core/Paper" 9 | import IconButton from "@material-ui/core/IconButton" 10 | import BuildIcon from "@material-ui/icons/Build" 11 | 12 | import ModalSetings from "../ModalSetings" 13 | import RoutesList from "../RoutesList" 14 | import RoutePlayground from "../RoutePlayground" 15 | 16 | import { 17 | setCurrentActiveRouteId, 18 | getCurrentActiveRouteId, 19 | getGlobalConfig 20 | } from "../../utils/storage" 21 | 22 | import { generateRouteId } from "../../utils/hash" 23 | 24 | import request from "../../utils/request" 25 | 26 | const APPBAR_HEIGHT = "64px" 27 | 28 | const useStyles = makeStyles(theme => ({ 29 | appName: { 30 | flexGrow: 1 31 | }, 32 | playground: { 33 | backgroundColor: theme.palette.background.default 34 | }, 35 | grid: { 36 | height: `calc(100vh - ${APPBAR_HEIGHT})` 37 | }, 38 | paper: { 39 | padding: theme.spacing(3, 2), 40 | textAlign: "center" 41 | } 42 | })) 43 | 44 | function App() { 45 | const classes = useStyles() 46 | const [globalHeaders, setGlobalHeaders] = useState([]) 47 | const [globalVariables, setGlobalVariables] = useState([]) 48 | const [data, setData] = useState({ config: { app_name: null }, routes: [] }) 49 | const [modalSettingsIsOpen, setModalSettingsIsOpen] = useState(false) 50 | const [selectedRoute, setSelectedRoute] = useState(null) 51 | 52 | useEffect(() => { 53 | updateSettingsData() 54 | request.get(window.api_info_url).then(({ data }) => { 55 | data.routes = data.routes.map(route => ({ 56 | ...route, 57 | __id: generateRouteId(route) 58 | })) 59 | setData(data) 60 | }) 61 | }, []) 62 | 63 | const updateSettingsData = useCallback(() => { 64 | setGlobalHeaders(getGlobalConfig("headers")) 65 | setGlobalVariables(getGlobalConfig("variables")) 66 | }, []) 67 | 68 | useEffect(() => { 69 | const routeId = getCurrentActiveRouteId() 70 | setSelectedRoute(routeId) 71 | }, []) 72 | 73 | const handleSetRoute = useCallback(routeId => { 74 | setCurrentActiveRouteId(routeId) 75 | setSelectedRoute(routeId) 76 | }, []) 77 | 78 | const openModalSettings = useCallback( 79 | () => setModalSettingsIsOpen(true), 80 | [] 81 | ) 82 | const closeModalSettings = useCallback( 83 | () => setModalSettingsIsOpen(false), 84 | [] 85 | ) 86 | 87 | const currentRoute = useMemo( 88 | () => 89 | !selectedRoute 90 | ? null 91 | : data.routes.find(route => route.__id === selectedRoute), 92 | [selectedRoute, data.routes] 93 | ) 94 | 95 | return ( 96 | 97 | 102 | 103 | 104 | 109 | {data.config.app_name 110 | ? `${data.config.app_name} API Explorer` 111 | : "Loading..."} 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 124 | 125 | 126 | {currentRoute ? ( 127 | 132 | ) : ( 133 | 139 | 140 | 141 | 142 | 143 | No route selected. 144 | 145 | 146 | Select a route on the list. 147 | 148 | 149 | 150 | 151 | 152 | )} 153 | 154 | 155 | ) 156 | } 157 | 158 | export default App 159 | -------------------------------------------------------------------------------- /resources/assets/js/components/App/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./App" 2 | -------------------------------------------------------------------------------- /resources/assets/js/components/ArgumentsList/ArgumentsList.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react" 2 | import PropTypes from "prop-types" 3 | import Box from "@material-ui/core/Box" 4 | import Button from "@material-ui/core/Button" 5 | import Grid from "@material-ui/core/Grid" 6 | import Input from "@material-ui/core/Input" 7 | import InputAdornment from "@material-ui/core/InputAdornment" 8 | import IconButton from "@material-ui/core/IconButton" 9 | import DeleteIcon from "@material-ui/icons/Delete" 10 | import Checkbox from "@material-ui/core/Checkbox" 11 | import { makeStyles } from "@material-ui/core/styles" 12 | 13 | import { argumentsList as argumentsListPropTypes } from "../../utils/sharedPropTypes" 14 | 15 | const useStyles = makeStyles(theme => ({ 16 | inputName: { 17 | marginTop: 10 18 | }, 19 | buttonAddItem: { 20 | marginTop: theme.spacing(1) 21 | } 22 | })) 23 | 24 | function ArgumentsList({ 25 | items, 26 | enabledAddArgument, 27 | onChangeValue, 28 | onChangeName, 29 | onAddArgument, 30 | onRemoveArgument, 31 | onToggleCheckArgument 32 | }) { 33 | const classes = useStyles() 34 | return ( 35 | 36 | {items.map(item => ( 37 | 38 | 39 | 45 | onChangeName(item.__id, e.target.value) 46 | } 47 | className={classes.inputName} 48 | inputProps={{ 49 | "aria-label": "description" 50 | }} 51 | /> 52 | 53 | 54 | 62 | onChangeValue(item.__id, e.target.value) 63 | } 64 | inputProps={{ 65 | "aria-label": "description" 66 | }} 67 | endAdornment={ 68 | 69 | 72 | onToggleCheckArgument(item.__id) 73 | } 74 | disabled={ 75 | !onToggleCheckArgument || 76 | item.disabledToggleCheck 77 | } 78 | value={true} 79 | color="primary" 80 | inputProps={{ 81 | "aria-label": "Enable/disable field" 82 | }} 83 | /> 84 | 85 | 92 | onRemoveArgument(item.__id) 93 | } 94 | > 95 | 96 | 97 | 98 | 99 | } 100 | /> 101 | 102 | 103 | ))} 104 | 113 | 114 | ) 115 | } 116 | ArgumentsList.defaultProps = { 117 | enabledAddArgument: true, 118 | onAddArgument: () => undefined, 119 | onRemoveArgument: null, 120 | onToggleCheckArgument: null, 121 | onChangeName: () => undefined 122 | } 123 | ArgumentsList.propTypes = { 124 | items: argumentsListPropTypes.isRequired, 125 | onChangeValue: PropTypes.func.isRequired, 126 | onChangeName: PropTypes.func, 127 | onAddArgument: PropTypes.func, 128 | onRemoveArgument: PropTypes.func, 129 | onToggleCheckArgument: PropTypes.func, 130 | enabledAddArgument: PropTypes.bool 131 | } 132 | 133 | export default ArgumentsList 134 | -------------------------------------------------------------------------------- /resources/assets/js/components/ArgumentsList/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ArgumentsList" 2 | -------------------------------------------------------------------------------- /resources/assets/js/components/ChipHttpVerb/ChipHttpVerb.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import Chip from "@material-ui/core/Chip" 4 | import { makeStyles } from "@material-ui/core/styles" 5 | import green from "@material-ui/core/colors/green" 6 | import blue from "@material-ui/core/colors/blue" 7 | import red from "@material-ui/core/colors/red" 8 | import orange from "@material-ui/core/colors/orange" 9 | import blueGrey from "@material-ui/core/colors/blueGrey" 10 | 11 | const useStyles = makeStyles(theme => ({ 12 | chip: { 13 | margin: theme.spacing(1) 14 | }, 15 | verb_GET: { background: blue[500] }, 16 | verb_POST: { background: green[500] }, 17 | verb_PUT: { background: blueGrey[500] }, 18 | verb_DELETE: { background: red[500] }, 19 | verb_PATCH: { background: orange[500] } 20 | })) 21 | 22 | function ChipHttpVerb({ verb }) { 23 | const classes = useStyles() 24 | return ( 25 | 30 | ) 31 | } 32 | 33 | ChipHttpVerb.propTypes = { 34 | verb: PropTypes.oneOf(["GET", "POST", "PUT", "DELETE", "PATCH"]).isRequired 35 | } 36 | 37 | export default ChipHttpVerb 38 | -------------------------------------------------------------------------------- /resources/assets/js/components/ChipHttpVerb/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ChipHttpVerb" 2 | -------------------------------------------------------------------------------- /resources/assets/js/components/DescriptionTable/DescriptionTable.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react" 2 | import PropTypes from "prop-types" 3 | import { makeStyles } from "@material-ui/core/styles" 4 | import Typography from "@material-ui/core/Typography" 5 | import Chip from "@material-ui/core/Chip" 6 | import Avatar from "@material-ui/core/Avatar" 7 | import Table from "@material-ui/core/Table" 8 | import Tooltip from "@material-ui/core/Tooltip" 9 | import TableBody from "@material-ui/core/TableBody" 10 | import TableCell from "@material-ui/core/TableCell" 11 | import TableHead from "@material-ui/core/TableHead" 12 | import TableRow from "@material-ui/core/TableRow" 13 | import HelpIcon from "@material-ui/icons/HelpOutline" 14 | import Divider from "@material-ui/core/Divider" 15 | 16 | const useStyles = makeStyles(theme => ({ 17 | routeInfoSection: { 18 | margin: theme.spacing(1) 19 | }, 20 | list: { 21 | width: "100%", 22 | backgroundColor: theme.palette.background.paper 23 | }, 24 | chip: { 25 | margin: theme.spacing(0.5) 26 | }, 27 | avatar: { 28 | background: theme.palette.grey[300] 29 | } 30 | })) 31 | 32 | const TableCellLabel = ({ content }) => ( 33 | 34 | {content} 35 | 36 | ) 37 | TableCellLabel.propTypes = { 38 | content: PropTypes.string.isRequired 39 | } 40 | 41 | const ErrorMsg = ({ text }) => { 42 | return ( 43 | 44 | {text} 45 | 46 | ) 47 | } 48 | ErrorMsg.propTypes = { 49 | text: PropTypes.string.isRequired 50 | } 51 | 52 | const TableCellValue = ({ content }) => { 53 | const classes = useStyles() 54 | const values = Array.isArray(content) ? content : [content] 55 | return ( 56 | 57 | {values.map(value => { 58 | const itemValue = value.split("\\").splice(-1, 1)[0] 59 | return ( 60 | 69 | 70 | 71 | 72 | 73 | ) 74 | } 75 | /> 76 | ) 77 | })} 78 | 79 | ) 80 | } 81 | TableCellValue.propTypes = { 82 | content: PropTypes.oneOfType([ 83 | PropTypes.string, 84 | PropTypes.arrayOf(PropTypes.string) 85 | ]) 86 | } 87 | 88 | const DescriptionTable = ({ 89 | title, 90 | items, 91 | emptyMsg, 92 | columnLabel, 93 | columnValue 94 | }) => { 95 | const classes = useStyles() 96 | const keys = useMemo(() => Object.keys(items), [items]) 97 | return ( 98 | 99 | 104 | {title} 105 | 106 | {keys.length < 1 ? ( 107 | 108 |

109 | 110 |

111 | 112 |
113 | ) : ( 114 | 115 | 116 | 117 | {columnLabel} 118 | {columnValue} 119 | 120 | 121 | 122 | {keys.map(key => ( 123 | 124 | 125 | 126 | 127 | ))} 128 | 129 |
130 | )} 131 |
132 | ) 133 | } 134 | 135 | DescriptionTable.propTypes = { 136 | title: PropTypes.string.isRequired, 137 | items: PropTypes.object.isRequired, 138 | emptyMsg: PropTypes.string.isRequired, 139 | columnLabel: PropTypes.string.isRequired, 140 | columnValue: PropTypes.string.isRequired 141 | } 142 | 143 | export default DescriptionTable 144 | -------------------------------------------------------------------------------- /resources/assets/js/components/DescriptionTable/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './DescriptionTable' -------------------------------------------------------------------------------- /resources/assets/js/components/InfoList/InfoList.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import List from "@material-ui/core/List" 4 | import ListItem from "@material-ui/core/ListItem" 5 | import ListItemText from "@material-ui/core/ListItemText" 6 | import Typography from "@material-ui/core/Typography" 7 | import { makeStyles } from "@material-ui/core/styles" 8 | 9 | const useStyles = makeStyles(theme => ({ 10 | list: { 11 | width: "100%", 12 | backgroundColor: theme.palette.background.paper 13 | } 14 | })) 15 | 16 | const EmptyMsg = () => ( 17 | 18 | No value 19 | 20 | ) 21 | 22 | const InfoList = ({ items }) => { 23 | const classes = useStyles() 24 | return ( 25 | 26 | {items.map(item => ( 27 | 28 | } 31 | /> 32 | 33 | ))} 34 | 35 | ) 36 | } 37 | InfoList.propTypes = { 38 | items: PropTypes.arrayOf( 39 | PropTypes.shape({ 40 | label: PropTypes.string.isRequired, 41 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 42 | }) 43 | ) 44 | } 45 | 46 | export default InfoList 47 | -------------------------------------------------------------------------------- /resources/assets/js/components/InfoList/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./InfoList" 2 | -------------------------------------------------------------------------------- /resources/assets/js/components/ModalSetings/ModalSetings.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { makeStyles } from "@material-ui/core/styles" 4 | import Modal from "@material-ui/core/Modal" 5 | 6 | import SettingsPanel from "./SettingsPanel" 7 | 8 | const useStyles = makeStyles(() => ({ 9 | modal: { 10 | display: "flex", 11 | alignItems: "center", 12 | justifyContent: "center" 13 | } 14 | })) 15 | 16 | function ModalConfig({ open, onRequestClose, onUpdateSettings }) { 17 | const classes = useStyles() 18 | return ( 19 | 20 |
21 | 25 |
26 |
27 | ) 28 | } 29 | 30 | ModalConfig.defaultProps = { 31 | onUpdateSettings: () => null 32 | } 33 | 34 | ModalConfig.propTypes = { 35 | open: PropTypes.bool.isRequired, 36 | onRequestClose: PropTypes.func.isRequired, 37 | onUpdateSettings: PropTypes.func 38 | } 39 | 40 | export default ModalConfig 41 | -------------------------------------------------------------------------------- /resources/assets/js/components/ModalSetings/SettingsPanel.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useMemo, useEffect } from "react" 2 | import PropTypes from "prop-types" 3 | import Card from "@material-ui/core/Card" 4 | import CardHeader from "@material-ui/core/CardHeader" 5 | import CardContent from "@material-ui/core/CardContent" 6 | import CardActions from "@material-ui/core/CardActions" 7 | import Tabs from "@material-ui/core/Tabs" 8 | import Tab from "@material-ui/core/Tab" 9 | import Button from "@material-ui/core/Button" 10 | 11 | import TabPanel, { a11yProps } from "../TabPanel" 12 | import ArgumentsList from "../ArgumentsList" 13 | import { 14 | addGlobalItem, 15 | updateGlobalItem, 16 | removeGlobalItem, 17 | getGlobalConfig 18 | } from "../../utils/storage" 19 | 20 | function SettingsPanel({ handleClose, onUpdateSettings }) { 21 | const [currentTab, setCurrentTab] = useState(0) 22 | const [variables, setVariables] = useState([]) 23 | const [headers, setHeaders] = useState([]) 24 | const handleChangeTab = (_, newValue) => setCurrentTab(newValue) 25 | 26 | useEffect(onUpdateSettings, [variables, headers]) 27 | 28 | const refresh = useMemo( 29 | () => ({ 30 | headers: () => setHeaders(getGlobalConfig("headers")), 31 | variables: () => setVariables(getGlobalConfig("variables")) 32 | }), 33 | [] 34 | ) 35 | 36 | const addItem = useCallback(type => { 37 | addGlobalItem(type) 38 | refresh[type]() 39 | }, []) 40 | 41 | const addVariable = () => addItem("variables") 42 | const addHeader = () => addItem("headers") 43 | 44 | const removeItem = useCallback((type, id) => { 45 | removeGlobalItem(type, id) 46 | refresh[type]() 47 | }, []) 48 | 49 | const removeVariable = id => removeItem("variables", id) 50 | const removeHeader = id => removeItem("headers", id) 51 | 52 | const updateField = useCallback((type, id, field, value) => { 53 | updateGlobalItem(type, id, field, value) 54 | refresh[type]() 55 | }, []) 56 | 57 | const toggleCheck = useCallback((type, id) => { 58 | const item = getGlobalConfig(type).find(item => item.__id === id) 59 | updateField(type, id, "checked", !item.checked) 60 | }, []) 61 | 62 | const toggleCheckVariable = id => toggleCheck("variables", id) 63 | const toggleCheckHeader = id => toggleCheck("headers", id) 64 | 65 | const editNameVariable = (id, value) => 66 | updateField("variables", id, "name", value) 67 | 68 | const editNameHeader = (id, value) => 69 | updateField("headers", id, "name", value) 70 | 71 | const editValueVariable = (id, value) => 72 | updateField("variables", id, "value", value) 73 | 74 | const editValueHeader = (id, value) => 75 | updateField("headers", id, "value", value) 76 | 77 | useEffect(() => { 78 | refresh.variables() 79 | refresh.headers() 80 | }, []) 81 | 82 | return ( 83 | 84 | 85 | 86 | 91 | 92 | 93 | 94 | 95 | 103 | 104 | 105 | 113 | 114 | 115 | 116 | 119 | 120 | 121 | ) 122 | } 123 | 124 | SettingsPanel.defaultProps = { 125 | onUpdateSettings: () => null 126 | } 127 | 128 | SettingsPanel.propTypes = { 129 | handleClose: PropTypes.func.isRequired, 130 | onUpdateSettings: PropTypes.func 131 | } 132 | 133 | export default SettingsPanel 134 | -------------------------------------------------------------------------------- /resources/assets/js/components/ModalSetings/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ModalSetings" 2 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/Drawer/DrawerResponse.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | 4 | import DrawerWrapper from "./DrawerWrapper" 5 | import DescriptionTable from "../../DescriptionTable" 6 | import InfoList from "../../InfoList" 7 | 8 | function DrawerResponse({ showDrawer, handleCloseDrawer, data }) { 9 | return ( 10 | 14 | 21 | 28 | 29 | ) 30 | } 31 | DrawerResponse.defaultProps = { 32 | response: null 33 | } 34 | DrawerResponse.propTypes = { 35 | showDrawer: PropTypes.bool.isRequired, 36 | handleCloseDrawer: PropTypes.func.isRequired, 37 | data: PropTypes.shape({ 38 | status: PropTypes.number.isRequired, 39 | duration: PropTypes.number.isRequired, 40 | statusText: PropTypes.string.isRequired, 41 | headers: PropTypes.object.isRequired 42 | }).isRequired 43 | } 44 | 45 | export default DrawerResponse 46 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/Drawer/DrawerRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | 4 | import DrawerWrapper from "./DrawerWrapper" 5 | import RouteInfo from "./RouteInfo" 6 | import { route as routePropType } from "../../../utils/sharedPropTypes" 7 | 8 | function DrawerRoute({ showDrawer, handleCloseDrawer, route }) { 9 | return ( 10 | 14 | 15 | 16 | ) 17 | } 18 | DrawerRoute.propTypes = { 19 | showDrawer: PropTypes.bool.isRequired, 20 | handleCloseDrawer: PropTypes.func.isRequired, 21 | route: routePropType 22 | } 23 | 24 | export default DrawerRoute 25 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/Drawer/DrawerWrapper.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import Box from "@material-ui/core/Box" 4 | import Typography from "@material-ui/core/Typography" 5 | import Button from "@material-ui/core/Button" 6 | import DrawerMUI from "@material-ui/core/Drawer" 7 | import { makeStyles } from "@material-ui/core/styles" 8 | 9 | const useStyles = makeStyles(theme => ({ 10 | buttonCloseDrawer: { 11 | margin: `${theme.spacing(1)}px 0` 12 | }, 13 | drawerContent: { 14 | minWidth: "25vw" 15 | } 16 | })) 17 | 18 | function DrawerWrapper({ showDrawer, handleCloseDrawer, children }) { 19 | const classes = useStyles() 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | 34 | 35 | 36 | 37 | ) 38 | } 39 | DrawerWrapper.propTypes = { 40 | showDrawer: PropTypes.bool.isRequired, 41 | handleCloseDrawer: PropTypes.func.isRequired, 42 | children: PropTypes.node.isRequired 43 | } 44 | 45 | export default DrawerWrapper 46 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/Drawer/RouteInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { makeStyles } from "@material-ui/core/styles" 3 | import Box from "@material-ui/core/Box" 4 | import Divider from "@material-ui/core/Divider" 5 | import Typography from "@material-ui/core/Typography" 6 | 7 | import DescriptionTable from "../../DescriptionTable" 8 | import InfoList from "../../InfoList" 9 | import { route as routePropType } from "../../../utils/sharedPropTypes" 10 | 11 | const useStyles = makeStyles(theme => ({ 12 | routeInfoSection: { 13 | margin: theme.spacing(1) 14 | } 15 | })) 16 | 17 | function RouteInfo({ route }) { 18 | const classes = useStyles() 19 | return ( 20 | 21 | 26 | Route info 27 | 28 | 45 | 46 | 53 | 60 | 61 | ) 62 | } 63 | 64 | RouteInfo.propTypes = { 65 | route: routePropType.isRequired 66 | } 67 | 68 | export default RouteInfo 69 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/JsonEditor.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react" 2 | import PropTypes from "prop-types" 3 | import hashSum from "hash-sum" 4 | import JSONEditor from "jsoneditor" 5 | import "jsoneditor/dist/jsoneditor.css" 6 | 7 | function JsonEditor({ content, onChange }) { 8 | const editorNodeRef = useRef(null) 9 | const editorRef = useRef() 10 | 11 | useEffect(() => { 12 | const options = { 13 | mode: "code", 14 | enableSort: false, 15 | enableTransform: false, 16 | onChangeText: onChange 17 | } 18 | editorRef.current = new JSONEditor( 19 | editorNodeRef.current, 20 | options, 21 | content 22 | ) 23 | 24 | editorRef.current.focus() 25 | }, []) 26 | 27 | useEffect(() => { 28 | if (!editorRef.current) { 29 | return 30 | } 31 | 32 | const currentContent = editorRef.current.get() 33 | 34 | if (hashSum(content) === hashSum(currentContent)) { 35 | return 36 | } 37 | 38 | editorRef.current.update(content) 39 | }, [content]) 40 | 41 | useEffect(() => { 42 | return () => editorRef.current && editorRef.current.destroy() 43 | }, []) 44 | 45 | return
46 | } 47 | 48 | JsonEditor.propTypes = { 49 | content: PropTypes.object.isRequired, 50 | onChange: PropTypes.func.isRequired 51 | } 52 | 53 | export default JsonEditor 54 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/JsonViewer.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react" 2 | import PropTypes from "prop-types" 3 | import JSONEditor from "jsoneditor" 4 | import "jsoneditor/dist/jsoneditor.css" 5 | 6 | function JsonViewer({ content }) { 7 | const viewerNodeRef = useRef(null) 8 | 9 | useEffect(() => { 10 | const options = { 11 | mode: "view", 12 | enableSort: false, 13 | enableTransform: false, 14 | modes: ["view", "preview"] 15 | } 16 | viewerNodeRef.current = new JSONEditor( 17 | viewerNodeRef.current, 18 | options, 19 | content 20 | ) 21 | }, []) 22 | 23 | useEffect(() => { 24 | viewerNodeRef.current && viewerNodeRef.current.set(content) 25 | }, [content]) 26 | 27 | return
28 | } 29 | 30 | JsonViewer.defaultProps = { 31 | content: null 32 | } 33 | 34 | JsonViewer.propTypes = { 35 | content: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) 36 | } 37 | 38 | export default JsonViewer 39 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/Panel.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import Card from "@material-ui/core/Card" 4 | import CardHeader from "@material-ui/core/CardHeader" 5 | import CardContent from "@material-ui/core/CardContent" 6 | import CardActions from "@material-ui/core/CardActions" 7 | import { makeStyles } from "@material-ui/core/styles" 8 | 9 | const useStyles = makeStyles(theme => ({ 10 | wrapper: { 11 | margin: theme.spacing(2) 12 | } 13 | })) 14 | 15 | function Panel({ title, children, actions }) { 16 | const classes = useStyles() 17 | return ( 18 | 19 | 20 | {children} 21 | {actions && {actions}} 22 | 23 | ) 24 | } 25 | Panel.defaultProps = { 26 | actions: null 27 | } 28 | Panel.propTypes = { 29 | title: PropTypes.node.isRequired, 30 | children: PropTypes.node.isRequired, 31 | actions: PropTypes.node 32 | } 33 | 34 | export default Panel 35 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/RequestArea.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from "react" 2 | import PropTypes from "prop-types" 3 | import { makeStyles } from "@material-ui/core/styles" 4 | import Tabs from "@material-ui/core/Tabs" 5 | import Tab from "@material-ui/core/Tab" 6 | import Button from "@material-ui/core/Button" 7 | 8 | import Panel from "./Panel" 9 | import ArgumentsList from "../ArgumentsList" 10 | import JsonEditor from "./JsonEditor" 11 | import TabPanel, { a11yProps } from "../TabPanel" 12 | import { argumentsList as argumentsListPropTypes } from "../../utils/sharedPropTypes" 13 | 14 | const useStyles = makeStyles(theme => ({ 15 | wrapper: { 16 | margin: theme.spacing(2) 17 | }, 18 | button: { 19 | margin: theme.spacing(1) 20 | } 21 | })) 22 | 23 | function RequestArea({ 24 | onMakeRequest, 25 | onCancelRequest, 26 | onChangeJsonBody, 27 | isRequesting, 28 | parameters, 29 | queryStrings, 30 | headers, 31 | jsonBody, 32 | onEditArgument, 33 | onAddArgument, 34 | onRemoveArgument, 35 | onToggleCheckArgument 36 | }) { 37 | const classes = useStyles() 38 | const [currentTab, setCurrentTab] = useState(0) 39 | const handleChangeTab = (_, newValue) => setCurrentTab(newValue) 40 | 41 | const handleChangeParameterValue = (id, value) => 42 | onEditArgument("parameters", "value", id, value) 43 | 44 | const handleChangeHeaderName = (id, value) => 45 | onEditArgument("headers", "name", id, value) 46 | 47 | const handleChangeHeaderValue = (id, value) => 48 | onEditArgument("headers", "value", id, value) 49 | 50 | const handleChangeQSName = (id, value) => 51 | onEditArgument("queryStrings", "name", id, value) 52 | 53 | const handleChangeQSValue = (id, value) => 54 | onEditArgument("queryStrings", "value", id, value) 55 | 56 | const handleAddArgumentQueryString = () => onAddArgument("queryStrings") 57 | 58 | const handleAddArgumentHeader = () => onAddArgument("headers") 59 | 60 | const handleRemoveArgumentQueryString = id => 61 | onRemoveArgument("queryStrings", id) 62 | 63 | const handleRemoveArgumentHeader = id => onRemoveArgument("headers", id) 64 | 65 | const handleToggleCheckArgumentQueryString = id => 66 | onToggleCheckArgument("queryStrings", id) 67 | 68 | const handleToggleCheckArgumentHeader = id => 69 | onToggleCheckArgument("headers", id) 70 | 71 | return ( 72 | 76 | 85 | 94 | 95 | } 96 | > 97 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 113 | 114 | 115 | 116 | 117 | 118 | 126 | 127 | 128 | 136 | 137 | 138 | ) 139 | } 140 | RequestArea.propTypes = { 141 | onMakeRequest: PropTypes.func.isRequired, 142 | onCancelRequest: PropTypes.func.isRequired, 143 | onChangeJsonBody: PropTypes.func.isRequired, 144 | isRequesting: PropTypes.bool.isRequired, 145 | parameters: argumentsListPropTypes.isRequired, 146 | queryStrings: argumentsListPropTypes.isRequired, 147 | headers: argumentsListPropTypes.isRequired, 148 | jsonBody: PropTypes.object.isRequired, 149 | onEditArgument: PropTypes.func.isRequired, 150 | onAddArgument: PropTypes.func.isRequired, 151 | onRemoveArgument: PropTypes.func.isRequired, 152 | onToggleCheckArgument: PropTypes.func.isRequired 153 | } 154 | 155 | export default RequestArea 156 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/ResponseArea.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, Fragment } from "react" 2 | import PropTypes from "prop-types" 3 | import Typography from "@material-ui/core/Typography" 4 | import Button from "@material-ui/core/Button" 5 | import CircularProgress from "@material-ui/core/CircularProgress" 6 | import { makeStyles } from "@material-ui/core/styles" 7 | 8 | import Panel from "./Panel" 9 | import DrawerResponse from "./Drawer/DrawerResponse" 10 | import JsonViewer from "./JsonViewer" 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | text: { 14 | color: theme.palette.grey[400] 15 | }, 16 | buttonOpenDrawer: { 17 | float: "right" 18 | }, 19 | progress: { 20 | margin: `${theme.spacing(2)}px auto`, 21 | display: "block" 22 | } 23 | })) 24 | 25 | function ResponseArea({ response, isRequesting }) { 26 | const classes = useStyles() 27 | const [showDrawer, setShowDrawer] = useState(false) 28 | const openDrawer = useCallback(() => setShowDrawer(true), []) 29 | const handlCloseDrawer = useCallback(() => setShowDrawer(false), []) 30 | return ( 31 | 34 | Response 35 | 44 | 45 | } 46 | > 47 | {!response && !isRequesting && ( 48 | 53 | Make a request 54 | 55 | )} 56 | 57 | {isRequesting && } 58 | 59 | {response && ( 60 | 61 | 62 | 72 | 73 | )} 74 | 75 | ) 76 | } 77 | ResponseArea.defaultProps = { 78 | response: null 79 | } 80 | ResponseArea.propTypes = { 81 | response: PropTypes.object, 82 | isRequesting: PropTypes.bool.isRequired 83 | } 84 | 85 | export default ResponseArea 86 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/RoutePlayground.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Fragment, 3 | useState, 4 | useEffect, 5 | useCallback, 6 | useMemo 7 | } from "react" 8 | import Paper from "@material-ui/core/Paper" 9 | import Typography from "@material-ui/core/Typography" 10 | import Button from "@material-ui/core/Button" 11 | import { makeStyles } from "@material-ui/core/styles" 12 | 13 | import ChipHttpVerb from "../ChipHttpVerb" 14 | import DrawerRoute from "./Drawer/DrawerRoute" 15 | import RequestArea from "./RequestArea" 16 | import ResponseArea from "./ResponseArea" 17 | 18 | import request from "../../utils/request" 19 | 20 | import { 21 | route as routePropType, 22 | argumentsList as argumentsListPropTypes 23 | } from "../../utils/sharedPropTypes" 24 | 25 | import { replaceAll } from "../../utils/string" 26 | 27 | import { 28 | generateFieldId, 29 | getRouteArguments, 30 | addRouteArgumentItem, 31 | updateRouteArgumentItem, 32 | removeRouteArgumentItem, 33 | toggleCheckRouteArgumentItem, 34 | updateRouteBodyJson, 35 | getCurrentActiveRouteId 36 | } from "../../utils/storage" 37 | 38 | const format = { 39 | parameters: (route, stored) => 40 | stored.map(item => ({ 41 | ...item, 42 | disabledName: true, 43 | placeholderValue: route.wheres[name] 44 | })), 45 | queryStrings: (route, stored) => stored, 46 | headers: (route, stored) => stored 47 | } 48 | 49 | function applyVariables(str, variables = []) { 50 | let newStr = str 51 | variables.forEach(v => { 52 | newStr = replaceAll(newStr, `$\{${v.name}}`, v.value) 53 | }) 54 | return newStr 55 | } 56 | 57 | function formatUrl(url, parameters, variables) { 58 | const urlParams = url.match(/\{(.*?)\}/g) 59 | 60 | if (!Array.isArray(urlParams)) { 61 | return url 62 | } 63 | 64 | let formatedUrl = url 65 | urlParams.forEach(param => { 66 | const name = param.match(/[a-zA-Z0-9_.]/g).join("") 67 | const parameter = parameters.find(p => p.name === name) 68 | const value = parameter ? parameter.value : "" 69 | formatedUrl = formatedUrl.replace(param, value) 70 | }) 71 | return applyVariables(formatedUrl, variables) 72 | } 73 | 74 | function formatBody(body, variables) { 75 | return JSON.parse(applyVariables(JSON.stringify(body), variables)) 76 | } 77 | 78 | function formatArguments(params, variables) { 79 | return params.reduce((acc, curr) => { 80 | const name = applyVariables(curr.name, variables) 81 | const value = applyVariables(curr.value, variables) 82 | return curr.checked && name && value ? { ...acc, [name]: value } : acc 83 | }, {}) 84 | } 85 | 86 | const useStyles = makeStyles(theme => ({ 87 | paper: { 88 | padding: theme.spacing(1), 89 | margin: theme.spacing(2) 90 | }, 91 | header: { 92 | margin: theme.spacing(1) 93 | }, 94 | buttonOpenDrawer: { 95 | float: "right" 96 | }, 97 | buttonCloseDrawer: { 98 | margin: `${theme.spacing(1)}px 0` 99 | }, 100 | drawerContent: { 101 | minWidth: "25vw" 102 | } 103 | })) 104 | 105 | function RoutePlayground({ route, globalHeaders, globalVariables }) { 106 | const classes = useStyles() 107 | const [source, setSource] = useState(null) 108 | const [responses, setResponse] = useState({}) 109 | const [showDrawer, setShowDrawer] = useState(false) 110 | const [isRequesting, setIsRequesting] = useState(false) 111 | 112 | const [parameters, setParameters] = useState([]) 113 | const [queryStrings, setQueryStrings] = useState([]) 114 | const [headers, setHeaders] = useState([]) 115 | const [body, setBody] = useState({}) 116 | 117 | const setState = useMemo( 118 | () => ({ 119 | parameters: setParameters, 120 | queryStrings: setQueryStrings, 121 | headers: setHeaders 122 | }), 123 | [route.__id] 124 | ) 125 | 126 | const allHeaders = useMemo( 127 | () => [ 128 | ...headers, 129 | ...globalHeaders.map(item => ({ 130 | ...item, 131 | disabledName: true, 132 | disabledValue: true, 133 | disabledDelete: true, 134 | disabledToggleCheck: true 135 | })) 136 | ], 137 | [headers, globalHeaders] 138 | ) 139 | 140 | const variables = useMemo( 141 | () => 142 | globalVariables 143 | .filter(v => v.checked) 144 | .map(v => ({ name: v.name, value: v.value })), 145 | [globalVariables] 146 | ) 147 | 148 | const handleChangeJsonBody = useCallback(content => { 149 | try { 150 | const body = JSON.parse(content) 151 | const currentRouteId = getCurrentActiveRouteId() 152 | if (currentRouteId) { 153 | updateRouteBodyJson(currentRouteId, body) 154 | setBody(body) 155 | } 156 | } catch (e) { 157 | // invalid json 158 | } 159 | }, []) 160 | 161 | useEffect(() => { 162 | const stored = getRouteArguments(route.__id) 163 | const storedParamsItems = stored.parameters.map(p => p.name) 164 | route.parameters 165 | .filter(p => !storedParamsItems.includes(p)) 166 | .forEach(param => { 167 | addRouteArgumentItem(route.__id, "parameters", { 168 | __id: generateFieldId(), 169 | name: param, 170 | value: "" 171 | }) 172 | }) 173 | }, [route.__id]) 174 | 175 | useEffect(() => { 176 | const stored = getRouteArguments(route.__id) 177 | setState.parameters(format.parameters(route, stored.parameters)) 178 | setState.queryStrings(format.queryStrings(route, stored.queryStrings)) 179 | setState.headers(format.headers(route, stored.headers)) 180 | setBody(stored.body) 181 | }, [route.__id]) 182 | 183 | const openDrawer = useCallback(() => setShowDrawer(true), []) 184 | const handlCloseDrawer = useCallback(() => setShowDrawer(false), []) 185 | 186 | const handleEditArgument = useCallback( 187 | (type, field, id, value) => { 188 | updateRouteArgumentItem(route.__id, type, id, field, value) 189 | const stored = getRouteArguments(route.__id) 190 | setState[type](format[type](route, stored[type])) 191 | }, 192 | [route.__id] 193 | ) 194 | 195 | const handleAddArgument = useCallback( 196 | type => { 197 | addRouteArgumentItem(route.__id, type, { 198 | __id: generateFieldId() 199 | }) 200 | const stored = getRouteArguments(route.__id) 201 | setState[type](format[type](route, stored[type])) 202 | }, 203 | [route.__id] 204 | ) 205 | 206 | const handleRemoveArgument = useCallback( 207 | (type, id) => { 208 | removeRouteArgumentItem(route.__id, type, id) 209 | const stored = getRouteArguments(route.__id) 210 | setState[type](format[type](route, stored[type])) 211 | }, 212 | [route.__id] 213 | ) 214 | 215 | const handleToggleCheckArgument = useCallback( 216 | (type, id) => { 217 | toggleCheckRouteArgumentItem(route.__id, type, id) 218 | const stored = getRouteArguments(route.__id) 219 | setState[type](format[type](route, stored[type])) 220 | }, 221 | [route.__id] 222 | ) 223 | 224 | const handleMakeRequest = useCallback(() => { 225 | setIsRequesting(true) 226 | const sourceToken = request.CancelToken.source() 227 | setSource(sourceToken) 228 | request({ 229 | method: route.http_verb.toLowerCase(), 230 | url: formatUrl(route.url, parameters, variables), 231 | params: formatArguments(queryStrings, variables), 232 | headers: formatArguments(allHeaders, variables), 233 | data: formatBody(body, variables), 234 | cancelToken: sourceToken.token, 235 | validateStatus: function() { 236 | return true 237 | } 238 | }) 239 | .then(response => { 240 | setResponse({ ...responses, [route.__id]: response }) 241 | setIsRequesting(false) 242 | }) 243 | .catch(() => { 244 | setIsRequesting(false) 245 | }) 246 | }, [route.__id, parameters, queryStrings, allHeaders, body, variables]) 247 | 248 | const handleCancelRequest = useCallback(() => { 249 | source && source.cancel() 250 | }, [source]) 251 | 252 | useEffect(() => { 253 | setIsRequesting(false) 254 | source && source.cancel() 255 | }, [route.__id]) 256 | 257 | useEffect(() => () => source && source.cancel(), []) 258 | 259 | return ( 260 | 261 | 262 | 263 | 264 | {route.uri} 265 | 273 | 274 | 275 | 289 | 293 | 298 | 299 | ) 300 | } 301 | 302 | RoutePlayground.propTypes = { 303 | route: routePropType.isRequired, 304 | globalHeaders: argumentsListPropTypes.isRequired, 305 | globalVariables: argumentsListPropTypes.isRequired 306 | } 307 | 308 | export default RoutePlayground 309 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutePlayground/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./RoutePlayground" 2 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutesList/RouteListItem.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import ListItem from "@material-ui/core/ListItem" 4 | import ListItemText from "@material-ui/core/ListItemText" 5 | import Divider from "@material-ui/core/Divider" 6 | import Typography from "@material-ui/core/Typography" 7 | 8 | import ChipHttpVerb from "../ChipHttpVerb" 9 | import { route as routePropType } from "../../utils/sharedPropTypes" 10 | 11 | function RouteListItem({ route, isSelected, onSelect }) { 12 | return ( 13 | 14 | onSelect(route.__id)} 17 | disabled={!route.exists} 18 | button 19 | > 20 | 23 | 24 | 29 | {route.uri} 30 | 31 | {route.name && ( 32 | 33 | {" "} 34 | 39 | {route.name} 40 | 41 | 42 | )} 43 | 44 | } 45 | secondary={ 46 | route.description && ( 47 | 48 | 53 | {route.description} 54 | 55 | 56 | ) 57 | } 58 | /> 59 | 60 | 61 | 62 | ) 63 | } 64 | 65 | RouteListItem.defaultProps = { 66 | isSelected: false 67 | } 68 | 69 | RouteListItem.propTypes = { 70 | route: routePropType.isRequired, 71 | isSelected: PropTypes.bool, 72 | onSelect: PropTypes.func.isRequired 73 | } 74 | 75 | export default RouteListItem 76 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutesList/RoutesList.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useMemo } from "react" 2 | import PropTypes from "prop-types" 3 | import { makeStyles } from "@material-ui/core/styles" 4 | import Box from "@material-ui/core/Box" 5 | import List from "@material-ui/core/List" 6 | import Paper from "@material-ui/core/Paper" 7 | import SearchIcon from "@material-ui/icons/Search" 8 | import IconButton from "@material-ui/core/IconButton" 9 | import InputBase from "@material-ui/core/InputBase" 10 | 11 | import { route as routePropType } from "../../utils/sharedPropTypes" 12 | import RouteListItem from "./RouteListItem" 13 | 14 | const APPBAR_HEIGHT = "64px" 15 | const FORM_HEIGHT = "64px" 16 | 17 | const useStyles = makeStyles(theme => ({ 18 | root: { 19 | paddingTop: theme.spacing(2) 20 | }, 21 | input: { 22 | marginLeft: 8, 23 | flex: 1 24 | }, 25 | iconButton: { 26 | padding: 10 27 | }, 28 | form: { 29 | padding: "2px 4px", 30 | display: "flex", 31 | margin: "0 auto", 32 | alignItems: "center", 33 | width: `calc(100% - ${theme.spacing(4)}px)` 34 | }, 35 | list: { 36 | height: `calc(100vh - ${APPBAR_HEIGHT} - ${FORM_HEIGHT} - ${theme.spacing( 37 | 2 38 | )}px)`, 39 | overflow: "auto" 40 | } 41 | })) 42 | 43 | function RoutesList({ routes, selected, onSelect }) { 44 | const classes = useStyles() 45 | const [searchTerm, setSearchTerm] = useState("") 46 | const onSearch = useCallback(({ target }) => setSearchTerm(target.value), [ 47 | routes 48 | ]) 49 | 50 | const filteredRoutes = useMemo(() => { 51 | if (!searchTerm) { 52 | return routes 53 | } 54 | return routes.filter( 55 | route => 56 | (route.name && route.name.includes(searchTerm)) || 57 | route.uri.includes(searchTerm) 58 | ) 59 | }, [routes, searchTerm]) 60 | 61 | return ( 62 | 63 | 64 | 71 | 72 | 73 | 74 | 75 | 76 | {filteredRoutes.map(route => ( 77 | 83 | ))} 84 | 85 | 86 | ) 87 | } 88 | 89 | RoutesList.defaultProps = { 90 | selected: null 91 | } 92 | 93 | RoutesList.propTypes = { 94 | selected: PropTypes.string, 95 | onSelect: PropTypes.func.isRequired, 96 | routes: PropTypes.arrayOf(routePropType) 97 | } 98 | 99 | export default RoutesList 100 | -------------------------------------------------------------------------------- /resources/assets/js/components/RoutesList/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./RoutesList" 2 | -------------------------------------------------------------------------------- /resources/assets/js/components/TabPanel/TabPanel.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Box from "@material-ui/core/Box" 3 | import PropTypes from "prop-types" 4 | 5 | export function a11yProps(id, index) { 6 | return { 7 | id: `${id}-tab-${index}`, 8 | "aria-controls": `${id}-tabpanel-${index}` 9 | } 10 | } 11 | 12 | function TabPanel({ children, value, index, id }) { 13 | return value === index ? ( 14 | 20 | {children} 21 | 22 | ) : null 23 | } 24 | 25 | TabPanel.propTypes = { 26 | children: PropTypes.node.isRequired, 27 | value: PropTypes.number.isRequired, 28 | index: PropTypes.number.isRequired, 29 | id: PropTypes.string.isRequired 30 | } 31 | 32 | export default TabPanel 33 | -------------------------------------------------------------------------------- /resources/assets/js/components/TabPanel/index.js: -------------------------------------------------------------------------------- 1 | export * from "./TabPanel" 2 | export { default } from "./TabPanel" 3 | -------------------------------------------------------------------------------- /resources/assets/js/main.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | 4 | import App from "./components/App" 5 | 6 | ReactDOM.render(, document.getElementById("app")) 7 | -------------------------------------------------------------------------------- /resources/assets/js/utils/hash.js: -------------------------------------------------------------------------------- 1 | import hashSum from "hash-sum" 2 | 3 | export function generateRouteId({ http_verb, uri }) { 4 | return `route_${hashSum({ http_verb, uri })}` 5 | } 6 | -------------------------------------------------------------------------------- /resources/assets/js/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | axios.interceptors.request.use( 4 | function(config) { 5 | config.metadata = { startTime: new Date() } 6 | return config 7 | }, 8 | function(error) { 9 | return Promise.reject(error) 10 | } 11 | ) 12 | 13 | axios.interceptors.response.use( 14 | function(response) { 15 | response.config.metadata.endTime = new Date() 16 | response.duration = 17 | response.config.metadata.endTime - 18 | response.config.metadata.startTime 19 | return response 20 | }, 21 | function(error) { 22 | error.config.metadata.endTime = new Date() 23 | error.duration = 24 | error.config.metadata.endTime - error.config.metadata.startTime 25 | return Promise.reject(error) 26 | } 27 | ) 28 | 29 | export default axios 30 | -------------------------------------------------------------------------------- /resources/assets/js/utils/sharedPropTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types" 2 | 3 | export const route = PropTypes.shape({ 4 | __id: PropTypes.string, 5 | action: PropTypes.string, 6 | controller: PropTypes.string, 7 | request_handler: PropTypes.string, 8 | description: PropTypes.string, 9 | exists: PropTypes.bool, 10 | http_verb: PropTypes.oneOf(["GET", "POST", "PUT", "PATCH", "DELETE"]), 11 | middlewares: PropTypes.arrayOf(PropTypes.string), 12 | name: PropTypes.string, 13 | parameters: PropTypes.array, 14 | rules: PropTypes.object, 15 | uri: PropTypes.string, 16 | url: PropTypes.string, 17 | wheres: PropTypes.object 18 | }) 19 | 20 | export const argumentsList = PropTypes.arrayOf( 21 | PropTypes.shape({ 22 | __id: PropTypes.string.isRequired, 23 | disabledName: PropTypes.bool, 24 | disabledValue: PropTypes.bool, 25 | disabledDelete: PropTypes.bool, 26 | disabledToggleCheck: PropTypes.bool, 27 | placeholderValue: PropTypes.string, 28 | name: PropTypes.string, 29 | value: PropTypes.string, 30 | checked: PropTypes.bool 31 | }) 32 | ) 33 | -------------------------------------------------------------------------------- /resources/assets/js/utils/storage.js: -------------------------------------------------------------------------------- 1 | function getRouteKey(routeId) { 2 | return `routeConfig:${routeId}` 3 | } 4 | 5 | function getGlobalKey(type) { 6 | return `global:${type}` 7 | } 8 | 9 | export function generateFieldId() { 10 | return `field_${window.performance.now()}` 11 | } 12 | 13 | export function getRouteArguments(routeId) { 14 | const key = getRouteKey(routeId) 15 | const config = window.localStorage.getItem(key) 16 | const defaultConfig = { 17 | parameters: [], 18 | queryStrings: [], 19 | headers: [], 20 | body: {} 21 | } 22 | 23 | if (!config) { 24 | return defaultConfig 25 | } 26 | 27 | try { 28 | return JSON.parse(config) 29 | } catch (e) { 30 | return defaultConfig 31 | } 32 | } 33 | 34 | export function addRouteArgumentItem(routeId, type, params) { 35 | const routeArgs = getRouteArguments(routeId) 36 | const newItemValue = [ 37 | ...routeArgs[type], 38 | { name: "", value: "", checked: true, ...params } 39 | ] 40 | persist(routeId, type, newItemValue) 41 | } 42 | 43 | export function updateRouteArgumentItem(routeId, type, itemId, field, value) { 44 | const routeArgs = getRouteArguments(routeId) 45 | const updateItem = item => 46 | item.__id === itemId ? { ...item, [field]: value } : item 47 | const newItemValue = routeArgs[type].map(updateItem) 48 | persist(routeId, type, newItemValue) 49 | } 50 | 51 | export function removeRouteArgumentItem(routeId, type, itemId) { 52 | const routeArgs = getRouteArguments(routeId) 53 | const newItemValue = routeArgs[type].filter(item => item.__id !== itemId) 54 | persist(routeId, type, newItemValue) 55 | } 56 | 57 | export function toggleCheckRouteArgumentItem(routeId, type, itemId) { 58 | const routeArgs = getRouteArguments(routeId) 59 | const newItemValue = routeArgs[type].map(item => 60 | item.__id === itemId ? { ...item, checked: !item.checked } : item 61 | ) 62 | persist(routeId, type, newItemValue) 63 | } 64 | 65 | function persist(routeId, type, newItemValue) { 66 | const routeArgs = getRouteArguments(routeId) 67 | const newConfigValue = { ...routeArgs, [type]: newItemValue } 68 | const configKey = getRouteKey(routeId) 69 | window.localStorage.setItem(configKey, JSON.stringify(newConfigValue)) 70 | } 71 | 72 | export function updateRouteBodyJson(routeId, content) { 73 | persist(routeId, "body", content) 74 | } 75 | 76 | export function setCurrentActiveRouteId(routeId) { 77 | window.localStorage.setItem("currentRoute", routeId) 78 | } 79 | 80 | export function getCurrentActiveRouteId() { 81 | return window.localStorage.getItem("currentRoute") 82 | } 83 | 84 | export function getGlobalConfig(type) { 85 | const key = getGlobalKey(type) 86 | const stored = window.localStorage.getItem(key) 87 | if (!stored) { 88 | return [] 89 | } 90 | 91 | try { 92 | return JSON.parse(stored) 93 | } catch (e) { 94 | return [] 95 | } 96 | } 97 | 98 | function setGlobalConfig(type, items) { 99 | const key = getGlobalKey(type) 100 | window.localStorage.setItem(key, JSON.stringify(items)) 101 | } 102 | 103 | export function updateGlobalItem(type, id, field, value) { 104 | const items = getGlobalConfig(type) 105 | const edited = items.map(item => 106 | item.__id === id ? { ...item, [field]: value } : item 107 | ) 108 | setGlobalConfig(type, edited) 109 | } 110 | 111 | export function addGlobalItem(type) { 112 | const items = getGlobalConfig(type) 113 | const id = `field_${window.performance.now()}` 114 | const added = [...items, { __id: id, checked: true, name: "", value: "" }] 115 | setGlobalConfig(type, added) 116 | } 117 | 118 | export function removeGlobalItem(type, id) { 119 | const items = getGlobalConfig(type) 120 | const filtered = items.filter(item => item.__id !== id) 121 | setGlobalConfig(type, filtered) 122 | } 123 | -------------------------------------------------------------------------------- /resources/assets/js/utils/string.js: -------------------------------------------------------------------------------- 1 | export function replaceAll(str, find, replace) { 2 | return str.replace( 3 | new RegExp(find.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"), "g"), 4 | replace 5 | ) 6 | } 7 | -------------------------------------------------------------------------------- /resources/views/main.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 17 | API explorer 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Facades/LaravelApiExplorer.php: -------------------------------------------------------------------------------- 1 | getRoutes(); 17 | 18 | foreach ($routes as $route) { 19 | $items[] = $this->formatRoute($route); 20 | } 21 | 22 | return $items; 23 | } 24 | 25 | public function getConfig() 26 | { 27 | return [ 28 | 'app_name' => config('app.name') 29 | ]; 30 | } 31 | 32 | private function getRoutes() 33 | { 34 | $routeCollection = Route::getRoutes(); 35 | 36 | $laravelRoutes = $routeCollection->getRoutes(); 37 | $dingoRoutes = $this->getDingoRoutes(); 38 | 39 | $allRoutes = array_merge($laravelRoutes, $dingoRoutes); 40 | 41 | return $this->filterRoutes($allRoutes); 42 | } 43 | 44 | private function getDingoRoutes() 45 | { 46 | 47 | if (!class_exists(\Dingo\Api\Routing\Router::class)) { 48 | return []; 49 | } 50 | 51 | $dingoRouter = app('Dingo\Api\Routing\Router'); 52 | $versions = $dingoRouter->getRoutes(); 53 | $routes = []; 54 | foreach ($versions as $version) { 55 | $routes[] = $version->getRoutes(); 56 | } 57 | 58 | $routes = collect($routes)->flatten()->toArray(); 59 | 60 | return $routes; 61 | } 62 | 63 | private function filterRoutes($routes) 64 | { 65 | $filtered = []; 66 | 67 | $match = value(config('laravelapiexplorer.match'), function($match) { 68 | if (is_iterable($match)) { 69 | return array_map(function ($match) { 70 | return trim($match, '/'); 71 | }, $match); 72 | } else { 73 | return trim($match, '/'); 74 | } 75 | }); 76 | $ignoreList = collect(config('laravelapiexplorer.ignore')); 77 | $ignoreList->push('laravelapiexplorer.view'); 78 | $ignoreList->push('laravelapiexplorer.info'); 79 | $ignoreList->push('laravelapiexplorer.asset'); 80 | 81 | foreach ($routes as $route) { 82 | $name = $route->getName(); 83 | $uri = trim($route->uri(), '/'); 84 | 85 | if ( 86 | !Str::is($match, $name) && 87 | !Str::is($match, $uri) 88 | ) { 89 | continue; 90 | } 91 | 92 | $ignore = $ignoreList->contains(function ($value) use ($name, $uri) { 93 | return $value == $name || $value == $uri; 94 | }); 95 | 96 | if ($ignore) { 97 | continue; 98 | } 99 | 100 | $filtered[] = $route; 101 | } 102 | 103 | return $filtered; 104 | } 105 | 106 | private function formatRoute($route) 107 | { 108 | $action = $route->getAction(); 109 | 110 | $method = null; 111 | $exists = true; 112 | $description = ''; 113 | $rules = new \stdClass(); 114 | $requestClass = null; 115 | $requestHandler = null; 116 | $controller = $action['controller'] ?? null; 117 | if ($controller) { 118 | 119 | if (strpos($controller, '@') !== false) { 120 | list($controller, $method) = explode('@', $action['controller'], 2); 121 | } else { 122 | $method = '__invoke'; 123 | } 124 | 125 | $exists = class_exists($controller) && method_exists($controller, $method); 126 | 127 | if ($exists) { 128 | $requestClass = $this->getRequestClass($controller, $method); 129 | $description = $this->getMethodDescription($controller, $method); 130 | } 131 | } 132 | 133 | $uri = $route->uri(); 134 | 135 | $httpVerb = collect($route->methods())->filter(function ($value) { 136 | return $value != 'HEAD'; 137 | })->first(); 138 | 139 | if ($requestClass) { 140 | $rules = $this->getRules($requestClass); 141 | $requestHandler = $requestClass->name; 142 | } 143 | 144 | return [ 145 | 'name' => $route->getName(), 146 | 'description' => $description, 147 | 'url' => url($uri), 148 | 'uri' => trim($uri, '/'), 149 | 'exists' => $exists, 150 | 'http_verb' => $httpVerb, 151 | 'controller' => $controller, 152 | 'action' => $method, 153 | 'middlewares' => $route->middleware(), 154 | 'parameters' => $route->parameterNames(), 155 | 'wheres' => $route->wheres ? $route->wheres : new \stdClass(), 156 | 'request_handler' => $requestHandler, 157 | 'rules' => $rules 158 | ]; 159 | } 160 | 161 | private function getMethodDescription($controller, $method) 162 | { 163 | $reflectionMethod = new ReflectionMethod($controller, $method); 164 | $comment = $reflectionMethod->getDocComment(); 165 | 166 | $pattern = "#([a-zA-Z]+\s*[a-zA-Z0-9, ()_].*)#"; 167 | preg_match_all($pattern, $comment, $matches, PREG_PATTERN_ORDER); 168 | $found = $matches[0]; 169 | 170 | return (isset($found[0]) && substr($found[0], 0, 1) != '@') ? $found[0] : ''; 171 | } 172 | 173 | private function getRequestClass($controller, $method) 174 | { 175 | $requestClass = null; 176 | $reflectionMethod = new ReflectionMethod($controller, $method); 177 | $params = $reflectionMethod->getParameters(); 178 | 179 | if (count($params)) { 180 | $parameter = new ReflectionParameter([$controller, $method], 0); 181 | $requestClass = $parameter->getType() && !$parameter->getType()->isBuiltin() 182 | ? new ReflectionClass($parameter->getType()->getName()) 183 | : null; 184 | } 185 | 186 | return $requestClass; 187 | } 188 | 189 | private function getRules($requestClass) 190 | { 191 | $rules = new \stdClass(); 192 | 193 | if ($requestClass->hasMethod('rules')) { 194 | $className = $requestClass->name; 195 | $reflectionMethod = new ReflectionMethod($className, 'rules'); 196 | $allRules = $reflectionMethod->invoke(new $className()); 197 | 198 | foreach ($allRules as $field => $rule) { 199 | $rules->$field = $this->formatRule($rule); 200 | } 201 | } 202 | 203 | return $rules; 204 | } 205 | 206 | private function formatRule($ruleset) 207 | { 208 | if (is_string($ruleset)) { 209 | $ruleset = explode('|', $ruleset); 210 | } 211 | 212 | $rules = []; 213 | 214 | foreach ($ruleset as $ruleItem) { 215 | $rule = $ruleItem; 216 | 217 | if (is_object($rule)) { 218 | $rule = get_class($ruleItem); 219 | } 220 | 221 | $rules[] = $rule; 222 | } 223 | 224 | return $rules; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/LaravelApiExplorerController.php: -------------------------------------------------------------------------------- 1 | laravelApiExplorer = $laravelApiExplorer; 16 | } 17 | 18 | public function getView() { 19 | return view('netojose::main'); 20 | } 21 | 22 | public function getInfo() { 23 | $routes = $this->laravelApiExplorer->loadRoutesInfo(); 24 | $config = $this->laravelApiExplorer->getConfig(); 25 | return [ 26 | 'routes' => $routes, 27 | 'config' => $config 28 | ]; 29 | } 30 | 31 | public function getAsset($file) { 32 | $root = __DIR__ . '/../resources/assets/build'; 33 | 34 | if(strpos($file, '..') !== false){ 35 | abort(403, 'Unauthorized action.'); 36 | } 37 | 38 | $filePath = $root . '/' . $file; 39 | 40 | $headers = [ 41 | 'Content-Type' => $this->getFileContentType($file) 42 | ]; 43 | 44 | if(!file_exists($filePath)){ 45 | abort(404); 46 | } 47 | 48 | return response()->file($filePath, $headers); 49 | } 50 | 51 | private function getFileContentType($file) 52 | { 53 | $array = explode('.', $file); 54 | $ext = end($array); 55 | $contentTypes = [ 56 | 'css' => 'text/css', 57 | 'js' => 'text/javascript' 58 | ]; 59 | 60 | return $contentTypes[$ext]; 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/LaravelApiExplorerServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadTranslationsFrom(__DIR__.'/../resources/lang', 'netojose'); 17 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'netojose'); 18 | // $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 19 | $this->loadRoutesFrom(__DIR__.'/routes.php'); 20 | 21 | // Publishing is only necessary when using the CLI. 22 | if ($this->app->runningInConsole()) { 23 | 24 | // Publishing the configuration file. 25 | $this->publishes([ 26 | __DIR__.'/../config/laravelapiexplorer.php' => config_path('laravelapiexplorer.php'), 27 | ], 'laravelapiexplorer.config'); 28 | 29 | // Publishing the views. 30 | /*$this->publishes([ 31 | __DIR__.'/../resources/views' => base_path('resources/views/vendor/netojose'), 32 | ], 'apiexplorer.views');*/ 33 | 34 | // Publishing assets. 35 | /*$this->publishes([ 36 | __DIR__.'/../resources/assets' => public_path('vendor/netojose'), 37 | ], 'apiexplorer.views');*/ 38 | 39 | // Publishing the translation files. 40 | /*$this->publishes([ 41 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/netojose'), 42 | ], 'apiexplorer.views');*/ 43 | 44 | // Registering package commands. 45 | // $this->commands([]); 46 | } 47 | } 48 | 49 | /** 50 | * Register any package services. 51 | * 52 | * @return void 53 | */ 54 | public function register() 55 | { 56 | $this->mergeConfigFrom(__DIR__.'/../config/laravelapiexplorer.php', 'laravelapiexplorer'); 57 | 58 | // Register the service the package provides. 59 | $this->app->singleton('laravelapiexplorer', function ($app) { 60 | return new LaravelApiExplorer; 61 | }); 62 | } 63 | 64 | /** 65 | * Get the services provided by the provider. 66 | * 67 | * @return array 68 | */ 69 | public function provides() 70 | { 71 | return ['laravelapiexplorer']; 72 | } 73 | } -------------------------------------------------------------------------------- /src/routes.php: -------------------------------------------------------------------------------- 1 | prefix($prefix)->name('laravelapiexplorer.')->group(function () { 8 | Route::get('/', 'LaravelApiExplorerController@getView')->name('view'); 9 | Route::get('info', 'LaravelApiExplorerController@getInfo')->name('info'); 10 | Route::get('assets/{file}', 'LaravelApiExplorerController@getAsset')->where('file', '^([a-z0-9_\-\.]+).(js|css|svg)$')->name('asset'); 11 | }); 12 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | module.exports = { 4 | entry: "./resources/assets/js/main.js", 5 | output: { 6 | filename: "bundle.js", 7 | path: path.resolve(__dirname, "resources/assets/build") 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | exclude: /node_modules/, 14 | loader: "babel-loader" 15 | }, 16 | { 17 | test: /\.css$/i, 18 | use: ["style-loader", "css-loader"] 19 | }, 20 | { 21 | test: /\.svg/, 22 | use: { 23 | loader: "svg-url-loader" 24 | } 25 | } 26 | ] 27 | }, 28 | devServer: { 29 | contentBase: path.resolve("src") 30 | } 31 | } 32 | --------------------------------------------------------------------------------